Types, sizeof & Memory Layout
C types are not abstractions — they’re specifications for how many bytes to read/write and how to interpret those bytes. sizeof returns the byte count at compile time.
Integer Type Widths
The C standard guarantees only minimum widths. On x86-64 Linux/macOS:
| Type | Min | Typical 64-bit | Use for |
|---|---|---|---|
char | 8 | 8 | Single bytes, strings. Signedness impl-defined. |
short | 16 | 16 | Rarely needed. |
int | 16 | 32 | General arithmetic — “natural” integer. |
long | 32 | 64 Linux/macOS, 32 Windows | Avoid: varies per OS. |
long long | 64 | 64 | When you need 64 bits portably. |
size_t | ptr-size | 64 | Array sizes, sizeof results, loop counts. |
The dangerous implication: code that assumes sizeof(long) == 8 works on Linux but silently truncates data on Windows. Always use <stdint.h> types when exact widths matter.
Fixed-Width Types (<stdint.h>)
uint8_t // exactly 8 bits, unsigned
int16_t // exactly 16 bits, signed
uint32_t // exactly 32 bits, unsigned
int64_t // exactly 64 bits, signedEnforce at compile time with _Static_assert:
_Static_assert(sizeof(uint32_t) == 4, "uint32_t must be 4 bytes");Integer Promotion and UAC
Integer promotion: any value narrower than int (a char, short, bit-field) is widened to int before arithmetic. This means char + char has type int, not char. The result can exceed CHAR_MAX.
Usual arithmetic conversions (UAC): when two operands have different types, the lower-ranked one converts to the higher-ranked:
int → unsigned int → long → unsigned long → long long → unsigned long long
The canonical trap:
int s = -1;
unsigned int u = 0;
if (s < u) { ... } // NEVER taken: s converts to UINT_MAX (4294967295)gcc warns with -Wsign-compare (part of -Wextra).
sizeof Trap
sizeof returns size_t (unsigned). Comparing it to a signed int triggers the same signed/unsigned conversion trap:
int n = -1;
if (n < sizeof(int)) { ... } // converts n to size_t → UINT64_MAX > 4: takes the branchAlways use size_t for sizes and loop indices over arrays.
Signed vs Unsigned Semantics
- Signed overflow: undefined behaviour. The compiler assumes it never happens and may eliminate code based on that assumption.
- Unsigned overflow: well-defined wrapping (modulo 2ⁿ). Use unsigned when you need defined wrap-around.
See undefined-behaviour for the compiler optimisation implications.
#define vs enum vs const
Three ways to name a constant — each with different tradeoffs:
#define BUF_SIZE 4096 // no type, no scope, no sizeof, no debugger
const int BUF_SIZE = 4096; // typed, scoped, debugger-visible, addressable
enum { BUF_SIZE = 4096 }; // typed as int, scoped, zero overhead — preferredenum enumerators are always type int. The enum type itself has an implementation-defined width (usually int).
Linux Kernel Annotation Layer
The Linux kernel defines __u8, __le32, __be64, etc. in include/linux/types.h and uses the sparse tool to enforce that endian-annotated types (__le32, __be32) are never mixed without explicit conversion calls. This pattern — using types to encode invariants and enforce them statically — is a direct application of what this chapter teaches. See ch02 Real-World Connection.
Appears In
- m01-c-is-not-java — lesson 1-3: Types, sizeof & Memory Layout
- PDF ch02 — full treatment: promotion, UAC, UB, implicit conversions, preprocessor role