C / TYPES AND REPRESENTATION
Fixed-width integers from stdint.h
Pick fixed-width types like int32_t and uint64_t deliberately, use their limit macros, and print them portably with the PRI macros from inttypes.h.
What you will learn
- Declare data that crosses a boundary with int32_t/uint64_t so its layout never shifts
- Print fixed-width values with PRId32/PRIu64 from <inttypes.h>, never a guessed %ld
- Reach for int_leastN_t or int_fastN_t when only range or speed matters
- Recall intN_t are typedefs, so uint8_t arithmetic still happens in int
Understanding Fixed-width integers from stdint.h
The built-in integer types promise only minimum ranges, so a struct describing a file header or a hardware register cannot be written portably with int and long alone. <stdint.h> adds typedef names that pin the width: int32_t is signed and exactly 32 bits, uint64_t is unsigned, exactly 64 bits, and has no padding bits, so its bit pattern is fully determined. Each name comes with limit macros of its own, INT32_MAX, UINT64_MAX, INT8_MIN, so you never hand-write a boundary you might get wrong.
These names are typedefs, not new types. On a 64-bit Linux build int32_t is spelled int and int64_t is spelled long, while on 64-bit Windows int64_t is long long. That is why %d or %ld is not a portable way to print them, and why <inttypes.h> supplies PRId32, PRIu64 and the SCN equivalents that expand to whatever specifier the target actually needs. It is also why a uint8_t is promoted to int the moment you compute with it: the guarantee describes storage, not the type the arithmetic happens in.
Exact width is the wrong tool for a plain loop counter, since forcing a 16-bit variable on a 64-bit machine can cost extra instructions. So stdint.h offers two more families: int_least16_t is the narrowest type with at least 16 bits, and int_fast32_t is whatever the implementation considers fastest among types of at least 32 bits, frequently 64 bits wide. intmax_t and uintmax_t are the widest integers available, and uintptr_t is wide enough to hold a void * and convert it back unchanged. Use the exact types when the layout is fixed by something outside your program, and the least/fast types when only the range matters.
<stdio.h>
<stdint.h>
<inttypes.h>
int main(void)
{
int32_t a = INT32_MAX;
uint32_t b = UINT32_MAX;
int64_t c = INT64_MIN;
printf("INT32_MAX = %" PRId32 "\n", a);
printf("UINT32_MAX = %" PRIu32 "\n", b);
printf("INT64_MIN = %" PRId64 "\n", c);
printf("int32_t is really: %s\n",
_Generic((int32_t)0, int: "int", short: "short",
long: "long", default: "other"));
printf("int64_t is really: %s\n",
_Generic((int64_t)0, int: "int", long: "long",
long long: "long long", default: "other"));
return 0;
}
intN_t and friends are typedefs that fix an integer's storage width on every target while remaining ordinary built-in types underneath, so printing and promotion follow whichever type the alias names.
Worked examples
Exact widths for packed data
Exact-width types make a bit layout predictable, but casts are still required because uint8_t values promote to int before the shift happens.
<stdio.h>
<stdint.h>
<inttypes.h>
static uint32_t pack(uint8_t r, uint8_t g, uint8_t b, uint8_t a)
{
return ((uint32_t)r << 24) | ((uint32_t)g << 16)
| ((uint32_t)b << 8) | (uint32_t)a;
}
int main(void)
{
uint32_t px = pack(0x12, 0x34, 0x56, 0x78);
printf("packed = 0x%08" PRIX32 "\n", px);
printf("red = 0x%02" PRIX8 "\n", (uint8_t)(px >> 24));
return 0;
}
Example explained
Line 1((uint32_t)r << 24) casts before shifting: r on its own promotes to int, and for any red value from 0x80 up, shifting it 24 places overflows a 32-bit signed int, which is undefined.
Line 2Once every operand is uint32_t the whole expression is 32-bit unsigned, so each byte lands in exactly the position the format string reads back.
Line 3%08 combined with PRIX32 pads to eight hex digits, which is how you can see that no byte fell off either end.
Line 4(uint8_t)(px >> 24) shifts inside uint32_t, where a count of 24 is well defined, and the cast keeps only the byte asked for.
The least, fast and max families
The minimum-width and fastest-width families promise only a lower bound, shown on a 64-bit Linux build where int_fast32_t turns out to be 64 bits.
<stdio.h>
<stdint.h>
<limits.h>
int main(void)
{
printf("int_least16_t: %2d bits\n", (int)(sizeof(int_least16_t) * CHAR_BIT));
printf("int_fast32_t: %2d bits\n", (int)(sizeof(int_fast32_t) * CHAR_BIT));
printf("intmax_t: %2d bits\n", (int)(sizeof(intmax_t) * CHAR_BIT));
printf("uintptr_t: %2d bits\n", (int)(sizeof(uintptr_t) * CHAR_BIT));
return 0;
}
Example explained
Line 1int_least16_t comes out at 16 bits because short already satisfies "at least 16", and the standard asks for the narrowest type that does.
Line 2int_fast32_t is 64 bits here: the library maps it to long because a full register word is usually cheaper to load and operate on than a 32-bit slice, so "fast" can mean bigger.
Line 3intmax_t is the widest signed integer the implementation has, and printing it needs PRIdMAX since nothing promises it is long long.
Line 4uintptr_t is 64 bits because it must hold a void * and convert it back unchanged, which on this target takes a full pointer's worth of bits.
Important notes
The exact-width names are optional in the standard: a machine whose bytes are 16 bits cannot supply int8_t, while int_leastN_t, int_fastN_t, intmax_t and uintmax_t are always present. Every mainstream desktop and server compiler provides the exact set.
The PRI and SCN macros live in <inttypes.h>, which includes <stdint.h> for you, and the _Generic lines in the main example need -std=c11 or later.
Common mistakes
Printing a uint64_t with %lu: it matches on 64-bit Linux, but on a 32-bit target uint64_t is unsigned long long, so printf reads the wrong number of bytes and that argument plus every later one prints garbage. PRIu64 expands to the right specifier per target.
Writing uint64_t big = 1 << 40; the shift is evaluated in int before anything is assigned, so the shift count exceeds the width of int and the result is undefined, typically 0. Use UINT64_C(1) << 40 or (uint64_t)1 << 40.
Assuming uint8_t arithmetic stays 8 bits: with uint8_t x = 0x0F, the expression ~x is an int equal to 0xFFFFFFF0, so if (~x == 0xF0) is false. You have to narrow the result back with (uint8_t)~x.
Try it yourself
Change, predict, then run
Declare a uint16_t set to UINT16_MAX and an int64_t set to INT64_MIN and print both with the correct PRI macros. Then print the int64_t with %ld as well and compare what your compiler warns about.
Open the C workspaceCheck your understanding
A counter stored in uint64_t prints correctly with printf("%lu\n", n) on 64-bit Linux but prints nonsense when the same file is built for a 32-bit ARM target. What went wrong?
- uint64_t is only 32 bits wide on a 32-bit platform, so the top half of the counter is lost
- uint64_t is a typedef for unsigned long long there, so %lu makes printf read the wrong argument size; PRIu64 expands to the specifier that target needs
- printf cannot format values above UINT32_MAX unless they are cast to double first
- The counter has to be declared volatile before printf can see all 64 bits of it
Show answer
The width guarantee is absolute: wherever uint64_t exists it is exactly 64 bits. What varies is which built-in type carries it, and printf specifiers name built-in types. %lu says unsigned long, which is 32 bits on a 32-bit ABI, so printf pulls too few bytes off the argument list and everything after it is misread too. Option 0 is the tempting answer because it blames the width, which is the one thing stdint.h does fix.