🧬 Types & Qualifiers, In Depth
sizeof: the measuring-tape operator
▶ Open the interactive lesson — free, no signupAsk malloc for the wrong number of bytes and your program tramples the memory next door — one of the most common ways C programs crash. sizeof is how code asks "exactly how big is this?", so every allocation fits and every loop knows where its array ends. You'll also meet the classic trap where it suddenly answers 8 when you were expecting 40.
sizeof looks like a function, but it's an operator — as much a part of the language as +. It yields the size in bytes of a type or expression, computed at compile time — worked out while your code compiles, before the program ever runs (with one exotic exception we'll meet at the end).
Parens: when you need them, when you don't
Two forms, one rule: parentheses are required around type names, optional around expressions.
#include <stdio.h>
int main(void) {
int x = 0;
double d = 0;
printf("%zu\n", sizeof x); /* expression: parens optional */
printf("%zu\n", sizeof(double)); /* type name: parens REQUIRED */
printf("%zu\n", sizeof d);
printf("%zu\n", sizeof(x + 1.5)); /* type of x+1.5 is double */
printf("%zu\n", sizeof(char)); /* by definition, exactly 1 */
return 0;
}$ gcc forms.c -o forms && ./forms 4 8 8 8 1 # sizes on a typical x86-64 Linux box — int and double may differ elsewhere
The result has type size_t — an unsigned type from <stddef.h>. Print it with %zu, never %d. And beware: because it's unsigned, sizeof x - 10 can silently become a gigantic positive number if sizeof x < 10.
🧠 Checkpoint: Which of these is a syntax error?
sizeof xsizeof(x)sizeof intsizeof(int)
Show answer
sizeof int — A bare type name needs parentheses: sizeof(int). Expressions work with or without. That asymmetry is the giveaway that sizeof is an operator, not a function call.
Arrays: the one place sizeof is magic…
Applied to an array name, sizeof reports the whole array — one of the few contexts where an array does not decay to a pointer. That gives us the classic element-count idiom:
▶ This spot has an interactive memgrid widget — open the interactive lesson to play with it.
#include <stdio.h>
#define LEN(a) (sizeof (a) / sizeof (a)[0])
int main(void) {
int primes[] = { 2, 3, 5, 7, 11, 13 };
printf("bytes: %zu\n", sizeof primes);
printf("count: %zu\n", LEN(primes));
for (size_t i = 0; i < LEN(primes); i++)
printf("%d ", primes[i]);
printf("\n");
return 0;
}…and the classic trap
🤔 What does this print on x86-64?void f(int arr[10]) { printf("%zu\n", sizeof arr); }int main(void){ int a[10]; printf("%zu\n", sizeof a); f(a); }
Classic trap — think first
40, then 8. In main, a is a real array: 10 × 4 = 40 bytes. In f, the parameter is really int *arr — the array decayed to a pointer at the call, so you get pointer size: 8. Modern compilers even warn: "sizeof on array function parameter will return size of pointer".
A function parameter declared int arr[10] is rewritten by the compiler to int *arr — the 10 is decoration. Inside the function, sizeof arr is the size of a pointer (8 on x86-64). The count idiom only works where the real array is in scope; functions must receive the length as a separate parameter.
🧠 Checkpoint: Why can't a function compute the length of an array it received as a parameter?
- sizeof is illegal inside functions
- Arrays are copied, and the copy forgets its size
- The parameter is really a pointer — the array decayed at the call site, its length never passed
- It can, with sizeof arr / sizeof arr[0]
Show answer
The parameter is really a pointer — the array decayed at the call site, its length never passed — C passes a pointer to the first element; no length travels with it. That is why every array-taking function in the standard library (memcpy, qsort, fwrite…) also takes a count parameter.
sizeof in malloc: the idiom that survives refactoring
#include <stdio.h>
#include <stdlib.h>
int main(void) {
size_t n = 1000;
int *p = malloc(n * sizeof *p); /* not sizeof(int)! */
if (!p) return 1;
double *grid = calloc(n, sizeof *grid); /* zeroed */
if (!grid) { free(p); return 1; }
printf("allocated %zu + %zu bytes\n",
n * sizeof *p, n * sizeof *grid);
free(grid);
free(p);
return 0;
}Why sizeof *p beats sizeof(int): if p later becomes long long *p, the allocation stays correct automatically. Naming the type again is a bug waiting for the day someone changes one and not the other.
The operand is never evaluated (almost)
#include <stdio.h>
int main(void) {
int i = 5;
printf("%zu\n", sizeof(i++)); /* type is int: 4 */
printf("i = %d\n", i); /* i is STILL 5! */
int n = 3;
int vla[n]; /* variable-length array */
printf("%zu\n", sizeof vla); /* runtime: 12 (the one
case sizeof evaluates) */
return 0;
}$ gcc noeval.c -o noeval && ./noeval 4 i = 5 12
sizeof only inspects the type of its operand, so side effects inside it simply don't run. The exception: variable-length arrays. sizeof of a VLA must measure at runtime, so a VLA operand is evaluated — the one crack in "sizeof is compile-time".
🧠 Checkpoint: After int i = 5; size_t s = sizeof(i++); what is i?
- 6
- 5
- Unspecified
- 4
Show answer
5 — The operand of sizeof is not evaluated (unless it is a VLA) — only its type matters. i++ never runs; i stays 5. A linter will rightly grumble about side effects inside sizeof.
▶ This spot has an interactive editor widget — open the interactive lesson to play with it.
You now know how big things are — next, the keywords that decide where and how long things live: the storage classes.
▶ Practice this lesson interactively (with live gcc)