Tipos enteros en C: tamaños, rangos y comportamiento

En C, cuando declaras una variable para almacenar números, no solo le das un nombre, sino que le asignas un tipo de dato. Este tipo le dice al compilador cuánta memoria debe reservar en la RAM (en bytes) y cómo debe interpretar esos bits (si son números positivos, negativos o caracteres). Los tipos enteros son los más básicos: desde un simple char que apenas ocupa un byte, hasta un long long capaz de guardar cantidades astronómicas.

El diseño de estos tipos responde a una necesidad de eficiencia. Un procesador no es “infinito”; tiene registros de un tamaño específico. Si intentas guardar un número gigantesco en un espacio diminuto, el sistema no tiene dónde poner el resto de los dígitos. Por eso, el estándar de C no te dice exactamente cuántos bits tendrá un int, sino que establece un mínimo garantizado. Por ejemplo, un int debe tener al menos 16 bits, aunque en la mayoría de las computadoras modernas (como las que usan arquitectura x86-64) verás que ocupa 32 bits.

Cuando programas, debes elegir el tipo según el rango de valores que esperas manejar. Si usas un char (que suele ser de 8 bits) para contar la población de un país, el número se “desbordará” casi inmediatamente. Por el contrario, usar un long long para contar los segundos transcurridos desde que te despertaste es un desperdicio innecesario de memoria. Si eliges mal, te enfrentarás al desbordamiento (overflow), un error donde el número “da la vuelta” y se convierte en un valor inesperado o, en el caso de números con signo, produce un comportamiento indefinido (undefined behavior), lo que significa que el programa podría fallar de formas impredecibles.

Para saber exactamente qué límites tiene el tipo que estás usando en tu máquina específica, debes consultar la cabecera <limits.h>, que contiene constantes como INT_MAX o CHAR_MIN.

#include <stdio.h>
#include <limits.h>

int main(void) {
    // 'char' es el tipo más pequeño. 
    // Nota: 'char' puede ser signed o unsigned según tu compilador.
    // Para estar seguros, usamos 'signed char' o 'unsigned char'.
    signed char s_char = -120;
    unsigned char u_char = 200;
    char default_char = 10;

    // 'short' suele tener 16 bits.
    short s_val = 32000;

    // 'int' es el tipo estándar para aritmética general.
    // Suele ser de 32 bits en sistemas modernos.
    int i_val = 2147483647;

    // 'long' tiene un comportamiento curioso:
    // En Windows x64 (LLP64) suele ser de 32 bits.
    // En Linux x64 (LP64) suele ser de 64 bits.
    long l_val = 4294967295L;

    // 'long long' es garantizado de al menos 64 bits desde C99.
    long long ll_val = 9223372036854775807LL;

    // Mostramos el tamaño en bytes de cada tipo usando 'sizeof'
    printf("--- Tamaños en memoria (bytes) ---\n");
    printf("char:      %zu byte(s)\n", sizeof(char));
    printf("short:     %zu byte(s)\n", sizeof(short));
    printf("int:       %zu byte(s)\n", sizeof(int));
    printf("long:      %zu byte(s)\n", sizeof(long));
    printf("long long: %zu byte(s)\n\n", sizeof(long long));

    // Mostramos los límites de los tipos para entender sus rangos
    printf("--- Límites (usando limits.h) ---\n");
    printf("CHAR_MIN:     %d\n", SCHAR_MIN);
    printf("CHAR_MAX:     %d\n", SCHAR_MAX);
    printf("INT_MAX:      %d\n", INT_MAX);
    printf("UINT_MAX:     %u\n", UINT_MAX);
    printf("LONG_MAX:     %ld\n", LONG_MAX);
    printf("LLONG_MAX:    %lld\n", LLONG_MAX);

    return 0;
}

Desglose del código

En el ejemplo anterior, hemos utilizado el operador sizeof para inspeccionar la memoria. Este operador es fundamental porque nos revela la realidad de la arquitectura actual, más allá de lo que dice el estándar.

Fíjate en la diferencia entre signed char y unsigned char. Mientras que el primero puede representar valores negativos (como s_char = -120), el segundo solo admite valores positivos, lo que permite que su rango llegue hasta 255 (u_char = 200). El tipo char a secas es un tipo “ambiguo” que el compilador trata como con signo o sin signo dependiendo de la configuración de tu sistema.

Al imprimir i_val, vemos el límite de un entero de 32 bits (INT_MAX). Si intentaras sumar 1 a i_val, el resultado sería impredecible debido al desbordamiento. Con ll_val, usamos el sufijo LL para indicarle al compilador que ese literal es un long long, evitando que el compilador intente meter un número gigante en un espacio de int antes de asignarlo.

Finalmente, la diferencia entre long y long long es la clave para la portabilidad. Como hemos visto, l_val puede comportarse de forma distinta si compilas en Windows o en Linux, por lo que si necesitas asegurar 64 bits sin importar la plataforma, la opción segura es long long.

El error frecuente

Un error clásico es la confusión entre tipos con signo y sin signo al realizar comparaciones o cálculos de límites.

// ERROR: Comparación peligrosa con unsigned
unsigned char u_val = 10;
if (u_val - 20 > 0) { 
    // Esto se ejecutará!
    // 10 - 20 en un unsigned char no es -10, es 246 (desbordamiento).
    printf("Esto parece positivo, pero es un error de lógica.\n");
}

Este error es especialmente traicionero porque no produce un error de compilación. El compilador simplemente hace la aritmética de bits y el resultado es un valor “envolvente” (wrap-around). Herramientas como UBSan (Undefined Behavior Sanitizer) pueden ayudarte a detectar desbordamientos en operaciones con tipos con signo, pero los errores con tipos unsigned suelen ser errores de lógica pura que solo se detectan mediante pruebas exhaustivas o análisis estático.

13

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio