The Heap & malloc/free
Memory that outlives the stack frame
- › Explain why the stack isn't enough and what the heap is for
- › State the contract of malloc, free, calloc, and realloc precisely
- › Describe, at a high level, how an allocator tracks free memory with chunk headers
- › Recognize the classic heap bugs: leak, double-free, use-after-free, and why they matter
Why the stack isn't enough
Stack memory is automatic and fast, but it has a fatal limit for many jobs: it dies when its function returns (the epilogue gives the frame back — day 14). So you cannot return a pointer to a local and expect it to survive; the memory is reused immediately. You also can't easily allocate an amount you only learn at runtime, or memory that must outlive the function that created it. For those you need the heap: a large region of memory you allocate from explicitly and that lives until *you* free it.
The four calls, and their exact contract
void *malloc(size_t n)** — return a pointer tonuninitialized bytes, orNULLif it can't.void *calloc(size_t count, size_t size)** — like malloc forcount*sizebytes, but zeroed,- and it checks the multiplication for overflow (safer for arrays).
void *realloc(void *p, size_t n)** — resize p's block tonbytes, moving/copying if needed,- returning the new address (day 21).
void free(void *p)** — return p's block to the allocator. After this, p is *dangling* — the- memory may be reused, so you must not touch it again.
The contract is a promise you must keep: every malloc gets exactly one free, and you never use a block after freeing it. Break that and the bugs are subtle and dangerous.
1int *make_range(int n) {2 int *a = malloc(n * sizeof(int)); // heap: survives after we return3 if (!a) return NULL; // ALWAYS check malloc's result4 for (int i = 0; i < n; i++) a[i] = i;5 return a; // safe: heap memory outlives this function6}7// caller must free() the returned pointer, exactly once, when done.Compare that to returning &local_array — which would be a bug, because the local dies at ret.
Heap memory is exactly the tool for "make something here, use it elsewhere, later." The cost: the
caller now *owns* that pointer and must free it. Ownership — who is responsible for the free — is the
central discipline of manual memory, and thinking about it explicitly is what separates solid C from
leaky C.
What the allocator does under the hood
malloc doesn't ask the OS for memory on every call — that would be far too slow. Instead the C
library manages a big region and hands out chunks of it. Just before the bytes it returns to you,
it keeps a small header recording the chunk's size and status; freed chunks are threaded onto
free lists (glibc groups small recently-freed chunks into a fast per-thread cache called the
tcache) so a later malloc of the same size can reuse one instantly.
┌──────────────┬───────────────────────────┐
│ size / flags│ your bytes (what malloc │
│ (header) │ returns a pointer to) -> │
└──────────────┴───────────────────────────┘
^header (metadata) ^ the pointer you get
free() writes bookkeeping (e.g. free-list links) INTO the
freed chunk's user area. That's why using memory after free
is dangerous: the allocator may have written its own data there.char *p = malloc(16); free(p); free(p); (a double-free), versus char *p = malloc(16);
free(p); p[0] = 'x'; (a use-after-free). In plain terms, why is each dangerous even though nothing
"looks" wrong?Own your memory, and prove it with valgrind
In heap.c: write make_range(n) that heap-allocates and fills an int array and returns it (checking malloc), a caller that uses and then frees it exactly once, and a realloc-based grow. Then DELIBERATELY introduce, in separate copies or #ifdef blocks, (a) a leak (forget to free), (b) a double-free, and (c) a use-after-free, and run each under valgrind to see exactly what it reports. Fix them and get a clean run. Uses only this lesson plus day 16/21.
$ gcc -O0 -g -Wall heap.c -o heap && ./heap$ valgrind --leak-check=full ./heap- › You always check malloc's return for NULL
- › The clean version has exactly one free per malloc and a clean valgrind report
- › You saw valgrind report the leak, the double-free, and the use-after-free
- › You can explain who 'owns' each pointer (who must free it)