📚 The Standard Library
limits.h, float.h & stdint.h: know your sizes
▶ Open the interactive lesson — free, no signupA file your program saves on Linux can come back garbled when read on Windows — the two systems literally disagree about how many bytes some numbers take. Today you get number types that are exactly the same size on every machine, which is how image files, save games, and internet messages stay readable on every device on Earth.
"How big is an int?" The honest C answer: it depends. The standard only guarantees minimums — int ≥ 16 bits, long ≥ 32, long long ≥ 64. Real machines disagree: on 64-bit Linux long is 64 bits, on 64-bit Windows it's 32! Today's headers turn that chaos into certainty.
limits.h: the actual numbers on this machine
#include <stdio.h>
#include <limits.h>
#include <float.h>
int main(void) {
printf("CHAR_BIT = %d bits per byte\n", CHAR_BIT);
printf("char : %d .. %d\n", CHAR_MIN, CHAR_MAX);
printf("short : %d .. %d\n", SHRT_MIN, SHRT_MAX);
printf("int : %d .. %d\n", INT_MIN, INT_MAX);
printf("long : %ld .. %ld\n", LONG_MIN, LONG_MAX);
printf("uint max : %u\n", UINT_MAX);
printf("int bits = %zu\n", sizeof(int) * CHAR_BIT);
printf("DBL_EPSILON= %g, DBL_DIG=%d\n", DBL_EPSILON, DBL_DIG);
return 0;
}$ gcc mylimits.c -o mylimits && ./mylimits CHAR_BIT = 8 bits per byte char : -128 .. 127 short : -32768 .. 32767 int : -2147483648 .. 2147483647 long : -9223372036854775808 .. 9223372036854775807 uint max : 4294967295 int bits = 32 DBL_EPSILON= 2.22045e-16, DBL_DIG=15 # note: char is signed here — that's implementation-defined!
CHAR_BIT is the number of bits in a byte — 8 everywhere you'll likely ever work, but the standard permits more (some DSPs use 16 or 32). The pattern sizeof(type) * CHAR_BIT gives a type's bit width portably.
🧠 Checkpoint: What does the C standard guarantee about int?
- Exactly 32 bits
- At least 16 bits
- The same size as long
- At least 32 bits
Show answer
At least 16 bits — Only a minimum: int must span at least −32767..32767 (16 bits). It happens to be 32 bits on modern desktops, but 16-bit ints are alive and well on microcontrollers — which is exactly why stdint.h exists.
float.h: precision limits
| macro | typical value | meaning |
|---|---|---|
FLT_MAX / DBL_MAX | 3.4×10³⁸ / 1.8×10³⁰⁸ | largest finite float / double |
FLT_EPSILON | 1.19×10⁻⁷ | smallest x where 1.0f + x ≠ 1.0f |
DBL_EPSILON | 2.22×10⁻¹⁶ | same for double — the yardstick for relative comparisons |
FLT_DIG / DBL_DIG | 6 / 15 | decimal digits that survive a round trip |
These are the numbers behind Part 0's "float ≈ 7 digits, double ≈ 15–16" — now you know where to look them up.
The fix: stdint.h fixed-width types
C99 ended the guessing game. When the exact width matters — file formats, network packets, embedded registers, overflow-sensitive math — say what you mean:
| type | meaning |
|---|---|
int8_t … int64_t, uint8_t … uint64_t | exactly N bits, two's complement (mandatory since C23!) — optional only on exotic hardware |
int_least8_t … | smallest type with at least N bits — always exists |
int_fast8_t … | fastest type with at least N bits (often plain int under the hood) |
intptr_t / uintptr_t | integer wide enough to round-trip a pointer — the only sanctioned way to store an address as an integer |
intmax_t / INT32_MAX … | the widest integer type, plus a MAX/MIN macro for every type above |
Printing them: inttypes.h
Here's the trap: what printf specifier matches int64_t? On 64-bit Linux it's long (%ld), on Windows it's long long (%lld) — hardcode either and the other platform breaks. inttypes.h provides format macros that expand to the right letters via string-literal concatenation:
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
int main(void) {
int32_t file_offset = 123456; /* exact 32 bits */
uint8_t flags = 0xC3; /* exact byte */
int64_t big = INT64_MAX;
int_fast16_t counter = 0; /* whatever's quick */
/* PRId64 expands to "ld" or "lld" as needed —
string concatenation glues it into the format: */
printf("big = %" PRId64 "\n", big);
printf("offset = %" PRId32 ", flags = 0x%" PRIX8 "\n",
file_offset, flags);
uintptr_t addr = (uintptr_t)&counter; /* pointer <-> int */
printf("counter lives at 0x%" PRIxPTR "\n", addr);
printf("sizeof int_fast16_t = %zu\n", sizeof counter);
return 0;
}$ gcc fixed.c -o fixed && ./fixed big = 9223372036854775807 offset = 123456, flags = 0xC3 counter lives at 0x7ffc9a2b4c2c sizeof int_fast16_t = 8 # fast16 chose a full 64-bit register on this machine!
🧠 Checkpoint: Why is printf("%ld", my_int64) unportable for an int64_t?
- %ld only prints 32 bits
- int64_t may be long on one platform and long long on another — the specifier must match the underlying type
- printf can’t print 64-bit values
- It’s fine — int64_t is always long
Show answer
int64_t may be long on one platform and long long on another — the specifier must match the underlying type — int64_t is a typedef for whichever native type is 64-bit: long on Linux/macOS, long long on Windows. A mismatched specifier is UB. PRId64 from inttypes.h expands to the correct letters on each platform.
stddef.h & stdbool.h: the little glue types
size_t— unsigned type ofsizeofand array indexing; print with%zuptrdiff_t— signed result of subtracting pointers; print with%tdNULL— the null pointer constant (since C23 you can — and should — writenullptr)bool / true / false— via stdbool.h historically; real keywords in C23, no header neededoffsetof(type, member)— byte offset of a member inside a struct, computed at compile time
offsetof makes struct padding (Part 2) visible. Recall why: an int wants a 4-aligned address, so the compiler inserts gap bytes:
#include <stdio.h>
#include <stddef.h>
struct packet {
char tag; /* 1 byte */
int value; /* 4 bytes, wants 4-alignment */
char flag; /* 1 byte */
};
int main(void) {
printf("offset of tag = %zu\n", offsetof(struct packet, tag));
printf("offset of value = %zu\n", offsetof(struct packet, value));
printf("offset of flag = %zu\n", offsetof(struct packet, flag));
printf("sizeof struct = %zu\n", sizeof(struct packet));
return 0;
}$ gcc offsets.c -o offsets && ./offsets offset of tag = 0 offset of value = 4 offset of flag = 8 sizeof struct = 12 # 6 bytes of data, 12 bytes of struct: 6 bytes of padding!
▶ This spot has an interactive memgrid widget — open the interactive lesson to play with it.
🧠 Checkpoint: offsetof(struct packet, value) above returned 4, not 1. Why?
- offsetof counts from 1
- int members always come first in memory
- The compiler inserted 3 padding bytes so the int starts at a 4-aligned offset
- sizeof(char) is 4 here
Show answer
The compiler inserted 3 padding bytes so the int starts at a 4-aligned offset — Alignment (Part 5!): a 4-byte int is placed at offsets divisible by 4 for efficient access. tag occupies offset 0, offsets 1–3 are padding, value starts at 4. offsetof is how you verify a layout matches a file format or wire protocol.
So which type do I use?
▶ This spot has an interactive flow widget — open the interactive lesson to play with it.
Default to int for small numbers, size_t for sizes/indexes, int64_t when values can get big, exact-width types at any binary boundary (files, network, hardware). Reach for unsigned types for bit manipulation — not just because a value "can't be negative" (unsigned underflow bugs in loop conditions are legion).
▶ This spot has an interactive editor widget — open the interactive lesson to play with it.
Types measured and pinned down — now for the standard library's wildest corner: signals that interrupt your program, and jumps that teleport across functions.
▶ Practice this lesson interactively (with live gcc)