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:

TypeMinTypical 64-bitUse for
char88Single bytes, strings. Signedness impl-defined.
short1616Rarely needed.
int1632General arithmetic — “natural” integer.
long3264 Linux/macOS, 32 WindowsAvoid: varies per OS.
long long6464When you need 64 bits portably.
size_tptr-size64Array 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, signed

Enforce 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 branch

Always 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 — preferred

enum 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