Cuando trabajas con tipos de ancho fijo de <stdint.h>, como int32_t o uint64_t, estás intentando garantizar que tus datos ocupen siempre el mismo número de bytes independientemente de la arquitectura. Sin embargo, esto crea un conflicto con las funciones de la familia printf y scanf. El problema radica en que estas funciones son variádicas y confían ciegamente en la cadena de formato para saber cuántos bytes extraer de los registros o de la pila. Si defines un int64_t que en tu arquitectura es un long, pero usas el especificador %d (que espera un int), estás entrando en el terreno del comportamiento indefinido (UB). El programa podría imprimir valores incorrectos, corromper la pila o, en el mejor de los casos, simplemente fallar en arquitecturas de 64 bits.
Para resolver esto, <inttypes.h> proporciona macros que expanden al especificador de formato exacto que el compilador necesita para ese tipo específico en esa arquitectura concreta. Por ejemplo, la macro PRId64 se expandirá a "ld" en sistemas donde int64_t es un long, o a "lld" donde es un long long. Aunque la sintaxis puede parecer ruidosa al principio, es la única forma de asegurar que tu código sea verdaderamente portable entre sistemas de 32 y 64 bits.
Para usar estas macros en un printf, debes aprovechar la concatenación de literales de cadena de C. Al escribir printf("Valor: %" PRId64 "\n", val);, el preprocesador une la cadena "Valor: %" con la expansión de la macro y la cadena "\n", resultando en una única cadena de formato válida para el compilador.
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
int main(void) {
// Datos con diferentes anchos fijos
uint64_t timestamp = 1672531200ULL;
int32_t status_code = -42;
uint16_t sensor_id = 0xAB;
void *ptr = (void *)0xDEADBEEF; // Nota: En un sistema real, no hardcodees direcciones
// Variables para scanf
int64_t user_input_id;
// 1. Uso de macros de formato para printf (salida)
// Usamos PRId64 para int64_t, PRId32 para int32_t, PRIu16 para uint16_t
// PRIx64 para hexadecimal de 64 bits, y PRIxPTR para punteros
printf("--- Log de Sistema ---\n");
printf("Timestamp: %" PRIu64 " (Unix epoch)\n", timestamp);
printf("Status: %" PRId32 " (signed 32-bit)\n", status_code);
printf("Sensor ID: 0x%04" PRIx16 "\n", sensor_id);
printf("Dirección: %" PRIxPTR "\n", ptr);
// 2. Uso de macros de conversión para scanf (entrada)
// SCNd32 expande al formato adecuado para leer un int32_t
printf("\nIntroduzca un ID de usuario (int64_t): ");
if (scanf("%" SCNd64, &user_input_id) != 1) {
fprintf(stderr, "Error al leer el ID.\n");
return 1;
}
printf("ID leído: %" PRId64 "\n", user_input_id);
return 0;
}
Análisis del código
En la salida de datos, observa cómo tratamos a timestamp. No podemos usar %lu porque no sabemos si uint64_t es un unsigned long o un unsigned long long. Al usar %" PRIu64 ", delegamos esa decisión al encabezado <inttypes.h>. Lo mismo ocurre con status_code y la macro PRId32.
Un detalle crucial es PRIxPTR. Los punteros no son tipos de ancho fijo; su tamaño depende de la arquitectura (4 bytes en 32 bits, 8 bytes en 64 bits). Usar %p es la forma estándar de imprimir punteros, pero si necesitas imprimir la dirección en formato hexadecimal sin el prefijo 0x que añade %p, la macro PRIxPTR es la forma correcta y segura.
En la sección de scanf, la macro SCNd64 es vital. A diferencia de printf, que solo necesita el formato, scanf necesita que el tamaño del buffer de destino coincida exactamente con el tipo de dato que el procesador espera recibir. Si intentaras leer un int64_t con %d, estarías intentando meter 64 bits de datos en un espacio de 32 bits, provocando un desbordamiento de pila o una corrupción de memoria inmediata.
El error frecuente
Un error muy común es intentar “ahorrar” código mediante el casting manual a tipos de gran tamaño. Por ejemplo:
int32_t valor = 500;
// Error: Esto es un compromiso que puede fallar en algunas ABIs
printf("Valor: %lld\n", (long long)valor);
Aunque castear a long long y usar %lld suele funcionar porque long long es garantizado como al menos 64 bits por el estándar, no es la forma “limpia” de trabajar con tipos de ancho fijo. Estás forzando una conversión innecesaria y rompiendo la semántica de tus tipos originales. Lo correcto es usar la macro correspondiente al tipo real. Si usas AddressSanitizer (-fsanitize=undefined), el compilador te alertará si detecta que estás intentando pasar un tipo de un tamaño distinto al que el especificador de formato indica, algo que ocurre constantemente cuando se mezcla %d con int64_t.
N° 19