stdint.h, limits.h & inttypes.h — know your integers
Facts per ISO C17 · C23 changes marked · UB = undefined behavior, ID = implementation-defined
Styled for paper — hit Ctrl+P and pin it above your desk.
"How big is an int?" The honest C answer: it depends — the standard only guarantees minimums. On 64-bit Linux long is 64 bits; on 64-bit Windows it's 32. These headers turn that chaos into certainty.
<stdint.h> — fixed-width types (C99)
| type | meaning | min / max macros | print with |
int8_t / uint8_t | exactly 8 bits, two's complement (C23 mandates two's complement; the typedefs are optional only on exotic hardware) | INT8_MIN INT8_MAX / UINT8_MAX | PRId8 / PRIu8, hex PRIx8 |
int16_t / uint16_t | exactly 16 bits | INT16_MIN INT16_MAX / UINT16_MAX | PRId16 / PRIu16 |
int32_t / uint32_t | exactly 32 bits | INT32_MIN INT32_MAX / UINT32_MAX | PRId32 / PRIu32 |
int64_t / uint64_t | exactly 64 bits | INT64_MIN INT64_MAX / UINT64_MAX | PRId64 / PRIu64 |
int_least8_t … uint_least64_t | smallest type with at least N bits — always exists | INT_LEAST8_MIN … | PRIdLEAST8 … |
int_fast8_t … uint_fast64_t | fastest type with at least N bits (often plain int — or a full register — under the hood) | INT_FAST8_MIN … | PRIdFAST8 … |
intptr_t / uintptr_t | integer wide enough to round-trip a pointer — the only sanctioned way to store an address as an integer (optional but ubiquitous) | INTPTR_MIN INTPTR_MAX / UINTPTR_MAX | PRIdPTR / PRIxPTR |
intmax_t / uintmax_t | the widest integer type | INTMAX_MIN INTMAX_MAX / UINTMAX_MAX | PRIdMAX, or %jd / %ju |
Constant macros: INT8_C(42), UINT64_C(0xFFFFFFFFFF) make a literal of (at least) the right type without guessing suffixes. Also in stdint.h: SIZE_MAX, PTRDIFF_MIN/MAX, WCHAR_MIN/MAX, WINT_MIN/MAX, SIG_ATOMIC_MIN/MAX. C23 adds *_WIDTH macros (bit widths) for every type.
<limits.h> — what this machine's basic types hold
| type | standard guarantees at least | typical 64-bit Linux (LP64) | macros | learn |
char | 8 bits (CHAR_BIT ≥ 8); signedness is ID! | 8 bits, signed | CHAR_BIT CHAR_MIN CHAR_MAX | → lesson |
signed char / unsigned char | −127..127 / 0..255 | −128..127 / 0..255 | SCHAR_MIN SCHAR_MAX UCHAR_MAX | |
short | 16 bits (−32767..32767) | 16 bits | SHRT_MIN SHRT_MAX USHRT_MAX | |
int | 16 bits — yes, really (alive and well on microcontrollers) | 32 bits | INT_MIN INT_MAX UINT_MAX | → lesson |
long | 32 bits | 64 bits — but 32 on 64-bit Windows! | LONG_MIN LONG_MAX ULONG_MAX | |
long long | 64 bits (C99) | 64 bits | LLONG_MIN LLONG_MAX ULLONG_MAX | |
Minimum magnitudes read ±32767, not ±32768, because C17 still permitted sign-magnitude and ones'-complement machines; C23 mandates two's complement (negative-numbers lesson). The portable bit-width pattern: sizeof(type) * CHAR_BIT. Float limits (FLT_EPSILON, DBL_MAX…) live in <float.h> — covered in the limits-stdint lesson.
<inttypes.h> — printing fixed-width types
What printf specifier matches int64_t? On 64-bit Linux it's long (%ld), on Windows long long (%lld) — hardcode either and the other platform is UB. The PRI macros expand to the right letters and glue in via string-literal concatenation:
| build the macro name | pieces | examples |
printf: PRI + conversion + width | conversion: d i u o x X (C23: b) · width: 8 16 32 64, LEAST8…, FAST8…, MAX, PTR | PRId64 PRIu32 PRIx8 PRIXMAX PRIxPTR |
scanf: SCN + conversion + width | conversion: d i u o x | SCNd64 SCNu32 SCNx16 |
| usage | notes |
printf("big = %" PRId64 "\n", big); | the macro is a string literal — adjacent literals concatenate at compile time |
printf("flags = 0x%08" PRIX32 "\n", f); | flags, width and precision still go before the macro |
scanf("%" SCNd64, &big); | SCN macros size the pointer target correctly |
strtoimax / strtoumax / imaxabs / imaxdiv | also live in inttypes.h — intmax_t cousins of strtol/abs/div |
size_t & ptrdiff_t (<stddef.h>)
| type | what it is | print | trap | learn |
size_t | unsigned type of sizeof and array indexing; max is SIZE_MAX | %zu | it can't go negative: for (size_t i = n-1; i >= 0; --i) loops forever. Count down with for (size_t i = n; i-- > 0;) | → lesson |
ptrdiff_t | signed result of subtracting two pointers into the same array | %td | subtracting pointers into different objects is UB | → lesson |
ssize_t is POSIX, not ISO C. Also in stddef.h: NULL, offsetof(type, member), max_align_t; C23 adds nullptr and makes bool/true/false real keywords (historically via <stdbool.h>).
Integer promotions & conversion rank — the 60-second version
| rule | statement |
| rank ladder | _Bool < char < short < int < long < long long (an enum has the rank of its compatible type) |
| integer promotions | anything ranked below int becomes int first (or unsigned int if int can't hold every value). Applies to arithmetic, shifts, unary + - ~, and variadic calls (where float also becomes double) |
| usual arithmetic conversions | after promotion: same type → done · same signedness → lower rank converts up · mixed signedness: if the unsigned side's rank ≥ the signed side's, both go unsigned; if the signed type can represent every unsigned value (e.g. long vs unsigned int on LP64), both go signed; otherwise both go to the unsigned version of the signed type |
| signed → unsigned | always defined: value wraps modulo 2ⁿ ((unsigned)-1 == UINT_MAX) |
| unsigned → signed, value doesn't fit | ID result (or an implementation-defined signal) — in practice it wraps, but the standard doesn't say so |
| signed arithmetic overflow | UB — INT_MAX + 1 is not "wraps around", it's undefined. Unsigned arithmetic wraps by definition |
| expression | result | why | learn |
-1 > 1u | true(!) | same rank, mixed signs → −1 converts to unsigned, becoming 4294967295 | → lesson |
sizeof(int) > -1 | false(!) | sizeof yields size_t; −1 converts to SIZE_MAX | → lesson |
'A' + 1 | int 66 | two chars added = the addition happens in int | → lesson |
uint8_t u = 0xFF; ~u == 0x00 | false(!) | ~u promotes to int first: ~255 is −256, not 0. Fix: (uint8_t)~u | → lesson |
unsigned char u = 255; u << 1 | 510 | promotion to int happens before the shift — no 8-bit wraparound | → lesson |
unsigned short a = 65535, b = 65535; a * b | UB on 32-bit-int platforms(!) | both promote to signed int; 65535 × 65535 overflows INT_MAX. Cast one side: (unsigned)a * b | → lesson |
Which type do I use?
| situation | use | why |
| exact bit layout — file format, network packet, hardware register | uint32_t, int64_t, … (stdint.h) | same size on every machine on Earth |
| a size, length, or array index | size_t | matches sizeof, malloc, strlen; print with %zu |
| values that can exceed ~2 billion | int64_t (or long long) | plain int is 32 bits on today's desktops — and may be 16 elsewhere |
| bit manipulation, masks, flags | unsigned types | shifts and overflow are fully defined on unsigned |
| storing a pointer as an integer | uintptr_t | the only sanctioned round-trip |
| everything else — small everyday numbers | plain int | the natural word; don't reach for unsigned just because a value "can't be negative" — unsigned underflow bugs in loop conditions are legion |
💡
The one-line habit: at any binary boundary say the width out loud (uint32_t); everywhere else keep it simple (int, size_t). And print fixed-width types only through their PRI… macros — a hardcoded %ld is a portability bug waiting for a Windows build.