Dynamic Memory & Lifetimes
malloc and destroyed by your code with free. The heap is where flexibility lives — and where leaks, dangling pointers, and double-frees are born. This page makes ownership a conscious act.
The Process Memory Layout
When your program runs, the operating system gives it one address space with distinct regions. Knowing them explains most C behavior — including why local variables vanish and why large arrays belong on the heap.
Figure 1 — the classic C memory map: text (code), data/BSS (globals), heap (malloc grows up), and the stack (grows down, one frame per call).
int global_init = 5; // lives in .data, alive for the whole run
static int global_zero; // lives in .bss (zero-filled), alive for the run
int main(void) {
int local = 7; // lives on the STACK, dies when main returns
int *p = malloc(64); // lives on the HEAP until you free(p)
free(p); // heap object destroyed here
return 0;
}
The stack grows downward and is strictly LIFO — that is exactly why you may not return a pointer to a local. The heap grows upward, outlives the function that created it, and must be managed by hand.
malloc, calloc, realloc, free
Four functions cover heap management. malloc reserves bytes (uninitialized); calloc reserves and zeroes (safer default); realloc resizes in place if possible, possibly moving the block; free releases exactly one malloc-family block. Every allocation returns NULL on failure — check it, and check the multiplication: malloc(n * sizeof(T)) can overflow for huge n.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int n = 5;
int *arr = calloc(n, sizeof(int)); // zero-initialized int vector
if (arr == NULL) { // allocation failed: handle it
fprintf(stderr, "out of memory\n");
return 1;
}
for (int i = 0; i < n; i++) {
arr[i] = (i + 1) * 10; // fill as if it were any array
}
n = 8; // grow it: realloc copies old bytes
int *grown = realloc(arr, (size_t)n * sizeof(int));
if (grown == NULL) { // realloc failed: arr is STILL valid
free(arr); // release the original block, bail
return 1;
}
arr = grown; // take ownership of the new block
printf("arr[7] = %d\n", arr[7]); // garbage — we only zeroed 5 slots
free(arr); // one free per successful realloc
return 0;
}
Note the realloc idiom: assign to a temporary first, because on failure realloc leaves the original block alive — overwriting your pointer would leak it.
The Three Classic Memory Bugs
Real-world C bugs reduce to three patterns. Learn to recognize them by smell — that is cheaper than by crash:
- Leak — you allocate but never free. The program's memory grows until the OS kills it. Fix: for every
mallocthere must be a matchingfreeon every path, including error paths. - Double free — you call
freetwice on one block. Fix: set the pointer toNULLafter freeing; freeingNULLis legal and harmless. - Use-after-free — you touch a block after freeing it. The bytes may still "work" for a while, which makes this the most dangerous bug: it is exploitable. Fix: never keep aliases to a freed block.
#include <stdlib.h>
int main(void) {
// LEAK: temporary never freed
{
int *tmp = malloc(100);
// ... no free(tmp) on this path
}
// DOUBLE FREE + USE-AFTER-FREE: the same pointer twice
int *p = malloc(10);
free(p);
p = NULL; // idiom: NULL the pointer right after freeing
free(p); // legal now — free(NULL) is a no-op
return 0;
}
The compiler cannot see any of these. The AddressSanitizer can — this is exactly the case for -fsanitize=address from the setup page. Add it during development and the three bugs above stop being mysteries.
Ownership — Who Frees?
Every heap block must have exactly one owner: the code that ultimately calls free. Decide the owner when you allocate, and write the free next to the malloc in your head. Two idioms keep ownership visible:
#include <stdlib.h>
#include <string.h>
// IDIOM 1: the caller transfers ownership of its input
char *shout(char *text) { // caller's buffer, caller frees
char *up = malloc(strlen(text) + 1);
if (up == NULL) return NULL;
for (size_t i = 0; text[i]; i++)
up[i] = (text[i] >= 'a' && text[i] <= 'z') ? text[i] - 'a' + 'A' : text[i];
up[strlen(text)] = '\0';
return up; // caller owns and must free(up)
}
Document the rule in the function comment: "returns a heap allocation; caller frees". Ambiguity about ownership is how leaks and double-frees sneak into a codebase.
Stack vs Heap — When to Choose
| Concern | Stack | Heap |
|---|---|---|
| Allocation | Automatic on entry | Explicit malloc |
| Speed | One pointer move | System call + bookkeeping |
| Size limit | ~1–8 MB, fixed per thread | Up to available RAM |
| Lifetime | Until the block ends | Until free |
| Error risk | Overflow (deep recursion) | Leaks, dangling, double-free |
Rule of thumb: small and short-lived objects go on the stack; large, long-lived, or size-unknown objects go on the heap. The collections page builds exactly this kind of heap-managed structure.
Next: error handling — the C way of admitting failure.