5.1.14 · D4C Programming

Exercises — Dynamic memory — malloc, calloc, realloc, free

2,621 words12 min readBack to topic

This page is a self-testing ladder for the parent topic. Each problem is graded from L1 (Recognition) up to L5 (Mastery). Read the problem, try it on paper first, then open the collapsible solution. After each level there is a [!mistake] callout dissecting the trap students most often fall into at that level.

Before you start, two quick reminders that every solution leans on:

Assumptions for all numeric answers on this page (so results are checkable): sizeof(int) = 4, sizeof(char) = 1, sizeof(double) = 8, and pointers/size_t are 8 bytes (a typical 64-bit machine).


Level 1 — Recognition

Goal: can you read a call and say what it does?

Exercise 1.1

State, in bytes, how much memory each call requests, assuming the sizes above.

int    *a = malloc(10 * sizeof(*a));
char   *b = calloc(32, sizeof(*b));
double *c = malloc(6 * sizeof(*c));
Recall Solution 1.1

Apply term by term.

  • a: sizeof(*a) = sizeof(int) = 4, so bytes.
  • b: calloc(32, sizeof(char)) = bytes, and all set to zero.
  • c: sizeof(*c) = sizeof(double) = 8, so bytes.

Note the contents: a and c are garbage (uninitialized), b is all zeros because it came from calloc.

Exercise 1.2

For each blank, name the single function that fits:

  1. "Reserve bytes, don't bother clearing them." ⟶ ______
  2. "Reserve bytes and set them all to zero." ⟶ ______
  3. "Make an existing block bigger, keep the old data." ⟶ ______
  4. "Hand a block back to the heap." ⟶ ______
Recall Solution 1.2
  1. malloc (fast, dirty memory)
  2. calloc (cleared allocate)
  3. realloc (resize, preserving contents)
  4. free (return to the heap)

Recognition drill:

What does calloc do that malloc does not?
It zeroes the memory and does the count × size multiplication for you.
After free(p), what value does p still hold?
The same address it held before — free does not change your pointer.

Level 2 — Application

Goal: can you write a correct, checked allocation?

Exercise 2.1

Write code that allocates room for n doubles (with n known only at runtime), fills element i with i / 2.0, then frees it correctly. Include the two safety steps the parent note insists on.

Recall Solution 2.1
#include <stdlib.h>
 
double *d = malloc(n * sizeof(*d));   // 1. bytes = n * sizeof(double)
if (d == NULL) return 1;              // 2. malloc can fail -> must check
for (int i = 0; i < n; i++)           // 3. malloc gives garbage -> initialize
    d[i] = i / 2.0;
free(d);                              // 4. avoid a memory leak
d = NULL;                             // 5. avoid a dangling pointer

The two "safety steps" are the NULL check (step 2) and the d = NULL after free (step 5). Skipping step 2 risks dereferencing NULL; skipping step 5 leaves a dangling pointer that a later free(d) or d[0] would turn into undefined behavior.

Exercise 2.2

You need 100 frequency counters, all starting at 0. Write the one-line allocation using the right function, and say how many total bytes it requests.

Recall Solution 2.2
int *freq = calloc(100, sizeof(*freq));
if (!freq) return 1;

calloc is correct because counters must start at zero — with malloc you'd have to add a memset or a loop. Total bytes: bytes, all zeroed.

Application drill:

Why check malloc's return before using it?
It returns NULL if the heap is exhausted; using NULL is undefined behavior.
Why is calloc the natural choice for counters?
Counters must begin at zero, which calloc guarantees.

Level 3 — Analysis

Goal: can you find the bug and explain the failure?

Exercise 3.1

This code compiles and often "works". Identify the bug and describe the concrete failure mode.

int *p = malloc(3 * sizeof(*p));
free(p);
p[0] = 7;           // <-- ?
Recall Solution 3.1

The bug is a use-after-free. After free(p), the three ints belong to the heap allocator again; p still holds the old address (its bits didn't change), so p[0] = 7 writes into memory the program no longer owns.

Failure mode: undefined behavior. The allocator may have already handed that block to another allocation, so your write silently corrupts someone else's data — or the allocator's own bookkeeping — producing a crash or a bug that surfaces far from here. Fix: free(p); p = NULL; — then p[0] = 7 becomes a clean NULL-dereference crash instead of silent corruption. A tool like Valgrind flags this instantly.

Exercise 3.2

Explain, in terms of who "owns" the old block, why the line below leaks memory when realloc fails.

v = realloc(v, cap * sizeof(*v));   // grow v
Recall Solution 3.2

On failure, realloc returns NULL but leaves the old block untouched and still valid. The assignment overwrites v with NULL, so the address of the old (still-allocated) block is now lost forever — nobody holds it, so it can never be freed. That is a memory leak.

Fix — use a temporary, commit only on success:

int *tmp = realloc(v, cap * sizeof(*v));
if (!tmp) { free(v); return 1; }   // old v still valid -> free it, or keep it
v = tmp;                           // commit only when we know it worked

Analysis drill:

On failure, does realloc free the old block?
No — the old block stays valid; that's exactly why the self-assign leaks.
What tool catches use-after-free and leaks at runtime?
Valgrind.

Level 4 — Synthesis

Goal: can you combine growth, copying, and safety into one correct routine?

Exercise 4.1

Trace the geometric-growth buffer below. Starting from cap = 2, list the value of cap at the moment each grow happens while pushing x = 0, 1, ..., 9 (10 pushes). How many times does a grow occur, and what is the final cap?

int cap = 2, len = 0;
int *v = malloc(cap * sizeof(*v));
for (int x = 0; x < 10; x++) {
    if (len == cap) {          // full?
        cap *= 2;              // double
        int *tmp = realloc(v, cap * sizeof(*v));
        if (!tmp) { free(v); return 1; }
        v = tmp;
    }
    v[len++] = x;
}
free(v);

The growth is easiest to see as a staircase — look at Figure s01, where each blue step is a doubling and the orange line is how many items we've stored.

Figure — Dynamic memory — malloc, calloc, realloc, free
Recall Solution 4.1

We grow only when len == cap (the buffer is exactly full), and each grow doubles cap.

before push x len cap full? grow to
0 0 2 no
1 1 2 no
2 2 2 yes → 4
3 3 4 no
4 4 4 yes → 8
5–7 5,6,7 8 no
8 8 8 yes → 16
9 9 16 no

Grows happen at len = 2, 4, 8, i.e. 3 grows, and cap goes 2 → 4 → 8 → 16. Final cap = 16.

Exercise 4.2

Why does doubling give amortized total copying, while "grow by +1 each time" gives ? Give the total number of element-copies each strategy performs to reach len = 8 from cap = 1.

Recall Solution 4.2

When realloc moves the block, it must copy the old elements into the new one. The cost is dominated by those copies.

Doubling (1 → 2 → 4 → 8): grows copy 1 + 2 + 4 = 7 elements total (you copy the current contents each time you double). In general the sum , which is — spread over pushes, that's per push amortized.

Grow by +1 (1 → 2 → 3 → ... → 8): each of the 7 grows copies all current elements: copies. In general , which is .

So doubling: 7 copies; +1 growth: 28 copies — and the gap widens fast as grows.

Synthesis drill:

Why keep len and cap as separate variables?
cap is space allocated, len is space used; you only grow when used catches up to allocated.
What makes geometric growth cheap overall?
Total copying is amortized instead of .

Level 5 — Mastery

Goal: can you reason about overflow, portability, and the exact wording of guarantees?

Exercise 5.1

A student writes malloc(count * sizeof(*p)) where count is a size_t that a user controls. On a 64-bit machine (size_t max ), with sizeof(*p) = 16, give the smallest count for which count * 16 overflows size_t, and explain the security danger.

Recall Solution 5.1

size_t overflow means count * 16 exceeds and wraps around modulo . The product first exceeds the max when So the smallest overflowing count is . At that value, count * 16 = 2^{64}, which wraps to 0.

Danger: malloc(0) may return a tiny (or NULL) block, but the program thinks it has elements and writes far past the block — a classic heap buffer overflow, exploitable for code execution. This is why the multiplication must be validated before allocating.

Exercise 5.2

The parent note stresses that "calloc is overflow-safe" is not guaranteed by the C standard. (a) What is the standard's stance? (b) Write a portable manual overflow check for count * size that works regardless of implementation.

Recall Solution 5.2

(a) Whether calloc(count, size) detects overflow of count * size is implementation-defined. Glibc, musl, and most quality libcs do return NULL on overflow, but a conforming minimal implementation need not — so relying on it is non-portable.

(b) Portable check — verify the division inverts the multiplication:

void *safe_alloc(size_t count, size_t size) {
    if (size != 0 && count > SIZE_MAX / size)
        return NULL;                 // would overflow -> refuse
    return malloc(count * size);     // now provably safe
}

The test count > SIZE_MAX / size is true exactly when count * size would exceed SIZE_MAX. We guard size != 0 first because dividing by zero is undefined; if size == 0 there's nothing to overflow.

Mastery drill:

Is calloc's overflow protection guaranteed by the C standard?
No — it is implementation-defined; treat it as a practical bonus, not a portable guarantee.
What single arithmetic test detects count * size overflow portably?
size != 0 && count > SIZE_MAX / size.

Wrap-up recall

Recall One sentence per function

malloc = fast dirty bytes; calloc = zeroed bytes + the count × size split; realloc = grow/shrink preserving data (use a tmp!); free = give it back exactly once, then = NULL.

Recall The three habits that prevent 90% of heap bugs
  1. Check the return for NULL. 2. free exactly once, then set the pointer to NULL. 3. Use sizeof(*p) and a tmp variable for realloc.

See also: Arrays vs Pointers, The Stack and the Heap.