Cuando programas en C, especialmente para sistemas embebidos o software de alto rendimiento, no puedes asumir que un int tiene siempre 32 bits o que un long es siempre de 64 bits. La arquitectura, el compilador y el ABI (Application Binary Interface) dictan tamaños distintos, lo que rompe la portabilidad y genera bugs de desbordamiento silenciosos. Para manejar esto, el lenguaje proporciona un conjunto de macros de límites definidas en stdint.h, limits.h y stddef.h.
Estas macros son constantes preprocesadas que representan los valores mínimos y máximos que un tipo puede albergar. Por ejemplo, INT32_MAX te da el límite superior de un int32_t, mientras que INT_MAX te da el límite de un int estándar (cuyo tamaño es variable). La lógica detrás de esto es abstraer la arquitectura: en lugar de hardcodear 2147483647, usas la macro, y el compilador inyecta el valor exacto que corresponde a esa máquina específica.
Debes usar estas macros siempre que tu código dependa de la precisión de un tipo (como en protocolos de red o archivos binarios) o cuando necesites verificar si una operación aritmética va a desbordar el tipo antes de ejecutarla. Si ignoras estas constantes y asumes tamaños fijos, el código fallará al compilarse en un microcontrolador de 16 bits o al ejecutarse en una arquitectura de 64 bits donde los tipos cambian. El error más crítico ocurre cuando realizas una operación que desborda el tipo antes de comparar el resultado con un límite; en ese momento, has incurrido en comportamiento indefinido (UB), y el programa puede comportarse de forma errática o incluso ser explotado mediante desbordamientos de búfer.
Para manejar la impresión y lectura de estos tipos de forma segura, inttypes.h ofrece las macros PRI y SCN. Dado que int32_t podría ser un int en una máquina o un long en otra, usar %d o %ld en un printf es una receta para el desastre. Las macros PRId32, PRIu64, etc., expanden a la cadena de formato exacta que el compilador espera para ese tipo específico. Además, para asignar literales grandes a tipos de ancho fijo sin ambigüedades, se usan macros como INT32_C(val), que asegura que el literal se trate como el tipo correcto desde el inicio.
/* Compilar con: gcc -std=c11 -Wall -Wextra -Wpedantic -D_POSIX_C_SOURCE=200809L -o example example.c */
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
#include <stddef.h>
#include <limits.h>
#include <stdbool.h>
/* Función para sumar de forma segura evitando desbordamiento (overflow) */
bool safe_add_int32(int32_t a, int32_t b, int32_t *res) {
if (res == NULL) return false;
/* Verificamos desbordamiento positivo: si a + b > MAX, entonces a > MAX - b */
if ((b > 0 && a > INT32_MAX - b) || (b < 0 && a < INT32_MIN - b)) {
return false;
}
*res = a + b;
return true;
}
int main(void) {
/* Uso de macros de límites de <stdint.h> */
int32_t valor_max = INT32_MAX;
int32_t valor_min = INT32_MIN;
uint64_t entero_grande = UINT64_MAX;
/* Uso de constantes tipadas para evitar ambigüedades con literales */
int32_t constante_segura = INT32_C(2147483647);
/* Uso de <stddef.h> para tamaños de memoria */
size_t tamano_maximo = SIZE_MAX;
/* Uso de <limits.h> para tipos estándar */
int valor_std = INT_MAX;
/* Uso de <inttypes.h> para impresión portátil */
printf("Límites de tipos de ancho fijo:\n");
printf(" int32_t: [%" PRId32 ", %" PRId32 "]\n", INT32_MIN, INT32_MAX);
printf(" uint64_t: [%" PRIu64 ", %" PRIu64 "]\n", 0ULL, UINT64_MAX);
printf(" constante: %" PRId32 "\n\n", constante_segura);
printf("Límites de tipos estándar:\n");
printf(" int: [%d, %d]\n", INT_MIN, INT_MAX);
printf(" size_t: (Maximo: %" SZEUX ")\n\n", tamano_maximo);
/* Demostración de suma segura */
int32_t a = INT32_MAX - 10;
int32_t b = 20;
int32_t resultado;
printf("Intentando sumar %d + %d...\n", a, b);
if (!safe_add_int32(a, b, &resultado)) {
printf("Error: ¡Desbordamiento detectado!\n");
} else {
printf("Resultado: %d\n", resultado);
}
return 0;
}
Análisis del código
En la función safe_add_int32, la lógica de detección de desbordamiento es crucial. No podemos simplemente hacer if (a + b > INT32_MAX), porque la operación a + b ya habría desbordado antes de la comparación. La condición a > INT32_MAX - b reordena la aritmética para realizar una comparación segura que no provoca comportamiento indefinido.
En el main, observamos el uso de %" PRId32 " dentro de printf. El preprocesador expande esto en el especificador de formato correcto (como \"d\" o \"ld\"), lo que garantiza que el motor de formato de stdio interprete correctamente los bytes en la pila o en los registros, independientemente de si el int32_t subyacente es tratado como un int o un long.
Para el tipo size_t, utilizamos el especificador %" SZEUX ", que es la macro de inttypes.h diseñada específicamente para imprimir size_t de forma portable. De igual manera, para uint64_t, se usa PRIu64. Nota cómo INT32_C(2147483647) asegura que el literal sea tratado directamente como un int32_t, lo cual es vital cuando trabajas con constantes muy grandes que podrían ser interpretadas como long o long long por el compilador, causando advertencias de pérdida de precisión o errores en expresiones de evaluación constante.
El error frecuente
El error más común es intentar detectar un desbordamiento después de que ha ocurrido.
/* ERROR: Comportamiento Indefinido */
int32_t a = 2147483640;
int32_t b = 10;
int32_t res = a + b; // <--- AQUÍ OCURRE EL DESBORDAMIENTO (UB)
if (res < a) { // <--- Demasiado tarde: el daño ya está hecho
// Error detectado... pero ya es tarde
}
Si el compilador detecta que a + b desborda un signed int, puede optimizar la instrucción if (res < a) eliminándola por completo, bajo la premisa de que “en un mundo ideal, la suma de dos números positivos nunca es menor que uno de ellos”. Para que tu código sea robusto, la comprobación debe ser algebraica y preventiva, como se mostró en la función safe_add_int32. Herramientas como UndefinedBehaviorSanitizer (UBSan) con la bandera -fsanitize=undefined detectarán este error inmediatamente durante la ejecución.
N° 18