This page is the drill-ground for the parent topic . We march through every kind of case a heap-allocation problem can throw at you: normal sizes, the number zero , degenerate (empty) requests, allocation failure , growing, shrinking , integer overflow of the byte count, a word problem, and an exam trap. Nothing is left unshown.
You should already have met Pointers in C , the sizeof operator , and The Stack and the Heap . If a symbol here feels unfamiliar, that's the cue to revisit those.
Definition Two words you'll meet in the overflow examples
size_t — an unsigned integer type (from <stddef.h>) that is exactly wide enough to hold the size of any object in bytes. malloc, calloc, and realloc all take their size argument as a size_t, and `sizeof` produces a size_t. Using size_t for sizes means your arithmetic uses the same width malloc expects.
SIZE_MAX — a constant (from <stdint.h>) equal to the ==largest value a size_t can hold==. On a 32-bit size_t that is 2 32 − 1 = 4294967295 . If a product would exceed SIZE_MAX it wraps around (starts back near 0) instead of getting bigger — that silent wrap is the overflow bug we guard against.
Think of every heap call as a point on a grid. The two axes that matter are "how much am I asking for?" (the size you request) and "what is the allocator's answer?" (a real block, or NULL). Here is the full grid — every cell gets a worked example below.
Cell
Situation
What's tricky about it
Covered by
C1
Normal malloc of n objects, success
deriving byte count, garbage init
Example 1
C2
calloc for zeroed data
why zero matters, two-arg form, its own multiply overflow
Example 2
C3
malloc(0) — the zero-size request
may return NULL or a freeable non-NULL
Example 3
C4
Allocation failure (NULL returned) + byte-count overflow
must check; product can wrap
Example 4
C5
realloc to grow , block moves
old pointer invalid after, copy happens
Example 5
C6
realloc to shrink
data past new size is lost
Example 6
C7
realloc(NULL, n), realloc(p, 0), realloc(NULL, 0) — degenerate edges
behave like malloc/free/malloc(0)
Example 7
C8
Word problem : read unknown-length input
combine grow-loop + free
Example 8
C9
Exam twist : spot the bug
self-assign leak + overflow
Example 9
The sign/quadrant idea from geometry maps here onto sizes : positive-normal (C1, C2), zero (C3, C7), and the "answer is nothing" case (C4). Growing vs shrinking (C5, C6) are the two directions of change. A hidden fourth axis is whether the product n * sizeof(*p) itself overflows before malloc ever sees it (C4, C9): even a "small-looking" request can wrap to a tiny number and hand you a block that is dangerously undersized.
The picture below is a map of that whole grid , so read it carefully:
The horizontal line is the size axis — how many bytes you ask for grows as you move right. The left end is size 0 , the middle is a normal request , the right end is "too big to satisfy."
The three coloured dots are the three answer-classes. The pink dot (far left) is the zero-size request (C3, C7) — the allocator may answer NULL or a freeable non-NULL. The blue dot (middle) is a normal success (C1 malloc, C2 calloc). The yellow dot (right) is a request so large the allocator gives up and returns NULL (C4).
The two arrows above the line are realloc: the blue arrow points right (grow, C5) and the pink arrow points left (shrink, C6) — realloc is literally movement along this size axis .
The small fold-under lane beneath the axis is the hidden fourth axis : it shows a large request n × size wrapping around back near zero. That is why a request that "should" land on the yellow dot can secretly land near the pink dot with a block far too small — the danger C4 and C9 warn about.
Worked example Example 1 — C1: normal
malloc, success
Statement. Allocate room for n = 6 doubles, fill each slot i with i * 1.5, print slot 4, then clean up. Assume sizeof(double) == 8 bytes.
Forecast: How many bytes does the call reserve, and what is arr[4]? Guess before reading.
int n = 6 ;
double * arr = malloc (n * sizeof ( * arr)); // Step 1
if (arr == NULL ) return 1 ; // Step 2
for ( int i = 0 ; i < n; i ++ ) // Step 3
arr [i] = i * 1.5 ;
printf ( " %f\n " , arr [ 4 ]); // Step 4
free (arr); // Step 5
arr = NULL ;
Compute the byte count. n * sizeof(*arr) = 6 * 8 = 48 bytes.
Why this step? malloc counts in bytes , not objects. sizeof(*arr) asks "how big is one thing arr points to?" — here a double, so 8.
Check for NULL. Why this step? The heap can be full; touching a NULL pointer is undefined behavior .
Initialize. Why this step? malloc hands you garbage — reading before writing is undefined.
Read slot 4. arr[4] = 4 * 1.5 = 6.0.
free then null. Why this step? Otherwise a memory leak ; nulling kills the dangling pointer.
Verify: bytes = 6 × 8 = 48 . Value = 4 × 1.5 = 6.0 . Units check: "objects × bytes/object = bytes." ✓
Worked example Example 2 — C2:
calloc for zeroed counters (and its own overflow)
Statement. Count how many times each vowel-position appears; you need 5 counters (a e i o u) all starting at 0, then increment position 2 twice. What is c[2] and what were all counters before any increment?
Forecast: With malloc the starting values would be garbage. What does calloc guarantee — and does it protect its own multiply?
int * c = calloc ( 5 , sizeof ( * c)); // Step 1: reserve 5 zeroed ints
if ( ! c) return 1 ; // Step 2: check NULL
c [ 2 ] ++ ; c [ 2 ] ++ ; // Step 3: two increments
free (c);
Step 1 — the two-argument call. calloc(5, sizeof(*c)) takes count = 5 and size = sizeof(*c) separately; it multiplies them together to get the total byte count for you , and writes zeros everywhere. Bytes reserved = 5 × sizeof(int) = 5 × 4 = 20 .
Why this step? A frequency table must start at 0; calloc guarantees that, whereas malloc would give garbage.
Step 2 — check NULL. Why this step? Same as always: the heap can be exhausted, and touching NULL is undefined behavior .
Step 3 — increment twice. The starting value is a guaranteed 0, so after two ++ the value is 2.
A precision warning about calloc's multiply. It is tempting to think calloc(count, size) is guaranteed overflow-safe because it does the count * size multiply internally. It is not guaranteed by the C standard — whether calloc detects that count * size overflows a size_t is implementation-defined . Most real libcs (glibc, musl) do return NULL on overflow, but a conforming-but-minimal one need not. For portable code, validate the product yourself (count > SIZE_MAX / size) exactly as in Example 4 before calling.
Verify: every counter starts at 0 ; c[2] = 0 + 1 + 1 = 2. Bytes reserved = 5 × 4 = 20 (with 4-byte int). ✓
Worked example Example 3 — C3: the
zero-size request malloc(0)
Statement. What does void *p = malloc(0); give you, and is free(p) legal?
Forecast: Is p always NULL? Can you dereference it?
What the standard says. malloc(0) may return either NULL or a unique non-NULL pointer that you must not dereference but may pass to free .
Why this step? The result is implementation-defined — you cannot assume one branch.
Therefore the safe pattern: treat the return like any allocation — check it, never read/write through it, and always free it.
Why this step? If it's non-NULL, skipping free leaks; if it's NULL, free(NULL) is a harmless no-op.
Verify (logical): free(NULL) is defined to do nothing, and any non-NULL result of malloc(0) is a valid argument to free. So free(malloc(0)); is safe in both allowed outcomes. ✓ (No number to plug — this is the degenerate cell.)
malloc(0) returns NULL, so I can skip free."
Why it feels right: many libraries do return NULL. Reality: others hand back a real, freeable pointer — skipping free then leaks. Fix: always free whatever malloc returns.
Worked example Example 4 — C4: allocation
failure and byte-count overflow
Statement. You request an absurd amount, malloc returns NULL. But there is a subtler trap: the multiplication n * sizeof(*a) can itself wrap around before malloc ever sees it. Suppose the intended request is n = 1000000000 ints.
Forecast: If you skip the NULL check and write a[0] = 1, what happens? And what if n * 4 overflows the type it's computed in?
size_t n = 1000000000 ; // Step 0 — use size_t for sizes
if (n > SIZE_MAX / sizeof ( int )) return 1 ; // Step 1 — overflow guard
int * a = malloc (n * sizeof ( * a)); // Step 2 — may still fail
if (a == NULL ) { // Step 3 — check
fprintf (stderr, "out of memory \n " );
return 1 ;
}
a [ 0 ] = 1 ; // reached only on success
free (a);
Use size_t, not long. Why this step? Object sizes and malloc's argument are size_t (defined in the callout at the top). On some platforms long is 32-bit , so long n = 1000000000L; n * sizeof(*a) would be computed in a 32-bit type and wrap to a tiny wrong value — malloc would then hand back a block far too small and a[i] writes corrupt the heap.
Guard the multiply. Why this step? n * sizeof(int) overflows exactly when n > SIZE_MAX / sizeof(int). Dividing SIZE_MAX by the element size tells you the largest n that cannot overflow; checking before multiplying is the only portable way to catch the wrap — you cannot detect it after it has already silently happened.
Attempt. Requested bytes = 1 0 9 × 4 = 4 × 1 0 9 (~4 GB). May exceed the heap → malloc returns NULL.
Check. Why this step? This is the guard between you and a NULL dereference; skipping it makes a[0] = 1 write through NULL → undefined behavior .
Verify: intended bytes = 1000000000 × 4 = 4000000000 . The overflow threshold on a 32-bit size_t (SIZE_MAX = 4294967295) is SIZE_MAX / 4 = 1073741823; since n = 1000000000 < 1073741823, the product 4000000000 fits in 32 bits here — but any n above that threshold would wrap, which the guard rejects. ✓
Worked example Example 5 — C5:
realloc to grow , block moves
Statement. You hold 3 ints {7, 8, 9} in a block. Grow it to hold 6 ints. Show that old contents survive and the pointer may change.
Forecast: After growing, is v[1] still 8? Is v still the same address?
int * v = malloc ( 3 * sizeof ( * v)); // Step 1
v [ 0 ] = 7 ; v [ 1 ] = 8 ; v [ 2 ] = 9 ;
int * tmp = realloc (v, 6 * sizeof ( * v)); // Step 2
if ( ! tmp) { free (v); return 1 ; } // Step 3
v = tmp; // Step 4
// v[0..2] still 7,8,9 ; v[3..5] uninitialized
free (v);
Start at 3. bytes = 3 × 4 = 12 .
realloc to 6. bytes = 6 × 4 = 24 . The allocator either extends in place, or allocates a new 24-byte block, copies the 12 old bytes, and frees the old one.
Why this step? realloc is the only call that grows while preserving the first min(old,new) bytes.
Use a tmp. Why this step? On failure realloc returns NULL but keeps the old block alive — assigning straight to v would lose the only pointer to it (a leak).
Commit. Old contents {7,8,9} are intact; slots 3–5 are garbage (uninitialized), so initialize before reading.
Verify: preserved region size = min ( 12 , 24 ) = 12 bytes = 3 ints, so v[0]=7, v[1]=8, v[2]=9 still hold. New size = 24 bytes. ✓
The figure below draws exactly this move: the blue block on top is the original 12 bytes holding 7 8 9; the yellow arrow shows those 12 bytes being copied down into the new 24-byte block; the three pink cells on the right are the fresh, uninitialized slots 3–5.
Worked example Example 6 — C6:
realloc to shrink
Statement. Same block {7, 8, 9, 10, 11} (5 ints). Shrink it to hold only 2 ints. What survives, what is lost?
Forecast: After shrinking to 2, can you still read v[3]?
int * v = malloc ( 5 * sizeof ( * v));
for ( int i = 0 ;i < 5 ;i ++ ) v [i] = 7 + i; // {7,8,9,10,11}
int * tmp = realloc (v, 2 * sizeof ( * v)); // shrink to 2 ints
if (tmp) v = tmp; // shrink rarely fails, but guard anyway
// valid slots now: v[0]=7, v[1]=8. v[2..4] are GONE.
free (v);
Shrink to 2. new bytes = 2 × 4 = 8 . The allocator keeps the first min(old,new)=8 bytes.
Why this step? Shrinking preserves only the leading part you kept room for.
What's lost. Reading v[3] after the shrink is now undefined behavior — those bytes are no longer yours.
Verify: preserved = min ( 20 , 8 ) = 8 bytes = 2 ints → v[0]=7, v[1]=8. Indices ≥ 2 are out of bounds of the 8-byte block. ✓
Worked example Example 7 — C7: the three
degenerate realloc edges
Statement. Explain precisely what these do:
realloc(NULL, 40), realloc(p, 0), and realloc(NULL, 0).
Forecast: Are these errors, or do they become other functions?
realloc(NULL, 40). The C standard states: "If ptr is a null pointer, the realloc function behaves like the malloc function for the specified size." So this is exactly malloc(40) — 40 fresh bytes.
Why this step? It lets you write one grow-loop that works even on the very first (empty) allocation, because realloc(NULL, …) seeds it like a malloc.
realloc(p, 0) (non-NULL p). The standard makes the result deliberately loose: it either frees the old object and returns a null pointer, or returns a non-null pointer that you may not use to access the object (there is no object of size 0 to access) — you may only pass it to free. In C23 realloc(p, 0) is even declared to be undefined behaviour if you rely on it as a free. Practical rule: never use realloc(p, 0) as a way to free; call free(p) explicitly instead.
Why this step? Because the return is not portably a usable pointer and not portably a free, mixing those meanings is exactly the trap — so we sidestep it.
realloc(NULL, 0) (both pointer NULL and size 0). By rule 1 (ptr is null → behaves like malloc) with a size of 0, this reduces to malloc(0) — it may return NULL or a unique non-NULL freeable pointer, and you handle it exactly like Example 3.
Why this step? This is the intersection of both degenerate axes at once, so both edge rules apply together — never assume NULL.
Verify (logical): realloc(NULL, 40) ≡ malloc(40) → 40 bytes. realloc(p,0) returns either NULL or a pointer usable only for free; realloc(NULL,0) ≡ malloc(0), whose result is implementation-defined. So the portable rule "check the return, use free to free" holds for all three. ✓
Worked example Example 8 — C8:
word problem — read an unknown number of ints
Statement. A file contains an unknown count of integers, one per line. Read them all into a dynamically grown array, then report the count and the sum. Suppose the file holds 10, 20, 30, 40, 50 (5 numbers).
Forecast: Starting capacity 2, how many doublings happen to fit 5 items, and what is the sum?
int cap = 2 , len = 0 , x;
int * v = malloc (cap * sizeof ( * v)); // seed the buffer
long sum = 0 ;
while ( scanf ( " %d " , & x ) == 1 ) { // read until input ends
if (len == cap) { // Step A: buffer full?
cap *= 2 ; // Step B: double capacity
int * tmp = realloc (v, cap * sizeof ( * v)); // Step C: safe grow
if ( ! tmp) { free (v); return 1 ; }
v = tmp;
}
v [len ++ ] = x; // Step D: store + advance len
sum += x;
}
printf ( "count= %d sum= %ld\n " , len, sum);
free (v);
Step A — Why check len == cap? len is how many ints you've stored ; cap is how many you have room for. You only pay to grow when the used count catches up to the allocated count — never earlier.
Step B — Why double (not +1)? Geometric growth makes the total copying cost across all n inserts O ( n ) amortized ; adding 1 each time would recopy the whole array on every insert, giving O ( n 2 ) .
Step C — Why the tmp + free(v) on failure? If realloc returns NULL the old block is still valid; committing only through tmp means a failure never orphans the old pointer (no leak), and on success v = tmp adopts the (possibly moved) block.
Step D — Why v[len++] = x? Store into the next free slot, then advance len so the next insert lands one further along. sum += x tallies as we go.
Capacity trace (WHAT IT LOOKS LIKE): insert item 1 → len 1 ≤ cap 2; item 2 → len 2 == cap 2 full. Item 3 arrives → double to cap 4, store. Item 4 → len 4 == cap 4 full → next... item 5 arrives → double to cap 8, store. So capacity went 2 → 4 → 8: two doublings , final cap = 8 ≥ 5.
Verify: doublings from cap 2 to hold 5 items: 2 → 4 → 8 = 2 doublings; final cap = 8 \ge 5. count = len = 5. sum = 10+20+30+40+50 = 150. ✓
Worked example Example 9 — C9:
exam twist — find the bug
Statement. This code aims to grow v and never leak. Two bugs hide here. Find them and give the corrected byte count for n = 100 ints.
// BUGGY
int * v = malloc ( 4 ); // (a) only 4 bytes — one int, not many
v = realloc (v, 100 * sizeof ( * v)); // (b) self-assign realloc
Forecast: Which line leaks on failure, and how big should the request be?
Bug (a): hard-coded, wrong initial size. malloc(4) reserves 4 bytes = room for one int, not the intended many. Byte counts must be derived : malloc(n * sizeof(*v)). And if n comes from input, guard n > SIZE_MAX / sizeof(*v) first (Example 4) so the multiply can't wrap.
Why this step? Deriving the count keeps the line correct if the type changes, and the guard kills the hidden fourth-axis overflow.
Bug (b): self-assign leak. v = realloc(v, …) overwrites v even when realloc returns NULL, orphaning the old block → leak. Fix with a tmp:
int * tmp = realloc (v, 100 * sizeof ( * v));
if ( ! tmp) { free (v); return 1 ; }
v = tmp;
Why this step? On failure the old block is still valid; keeping its address in v (via tmp) means you can still free(v).
Correct byte count. For 100 ints: 100 × 4 = 400 bytes.
Why this step? This is the value the corrected malloc/realloc should request.
Verify: intended bytes = 100 × 4 = 400 . The self-assign version leaks exactly the old block whenever realloc fails; the tmp pattern keeps that block reachable so free(v) still works. ✓
Recall (C3) What are the two legal outcomes of
malloc(0)?
Either NULL, or a unique non-NULL pointer you must not dereference but may free. Always free the result.
Recall (C2/C4) What is
SIZE_MAX, and how do you guard count * size against overflow?
SIZE_MAX is the largest value a size_t can hold. Guard with if (count > SIZE_MAX / size) ... before multiplying — the wrap cannot be detected after it happens. calloc's own overflow check is only implementation-defined, not guaranteed.
Recall (C4) What is the value of a pointer after
malloc fails, and why compute sizes in size_t?
After failure the pointer is exactly NULL (integer 0) — always check first. Use size_t (not long) so n * sizeof(*p) is computed in the same width malloc expects and cannot silently wrap on 32-bit long platforms.
Recall (C5 vs C6) After
realloc, how many bytes of old data are guaranteed intact?
min(old_size, new_size) bytes. Growing keeps everything; shrinking keeps only the leading kept region.
Recall (C7) What do
realloc(NULL, n) and realloc(NULL, 0) do?
realloc(NULL, n) behaves like malloc(n) — a fresh allocation. realloc(NULL, 0) reduces to malloc(0) — may return NULL or a freeable non-NULL pointer.
Recall (C8) Starting at capacity 2 and doubling, how many doublings to fit 5 items?
Two: 2 → 4 → 8.