Stack vs heap
A managed language gave you one undifferentiated "memory" and a garbage collector that
decided when things died. Underneath, every program splits its working memory into two
regions with opposite rules. The stack is automatic, last-in-first-out storage tied
to function calls: each call gets a frame, and it vanishes the instant the call returns
— you never free it. The heap is a large pool you manage by hand with malloc and
free: a block lives exactly as long as you keep it alive, across any number of calls.
Stack is fast and finite; heap is flexible and slower. Knowing which is which — and who
frees what — is the difference between correct C and crashes.
The reset: the GC is gone. Stack memory frees itself when a function returns whether you like it or not (so a pointer into it dies there). Heap memory frees never unless you call
free(so forgetting leaks it). Lifetime is now your job.
One address space, several regions
A running process sees one flat virtual address space, carved into regions. From low addresses to high:
| Region | Holds | Grows | Lifetime |
|---|---|---|---|
| Text (code) | machine instructions | fixed | whole program |
Data (.data) |
initialized globals/statics | fixed | whole program |
BSS (.bss) |
zero-initialized globals/statics | fixed | whole program |
| Heap | malloc'd blocks |
upward ↑ | until you free |
| (gap) | unused virtual space | — | — |
| Stack | call frames, locals | downward ↓ | per function call |
Heap and stack grow toward each other from opposite ends of the gap — a layout chosen so each can expand without a fixed boundary between them. The hierarchy note was about speed of memory; this is about organization of one process's address space. (Virtual memory — why every process sees its own clean version of this map — gets its own note later.)
The stack: automatic storage, LIFO
The stack is managed by one register, rsp (the stack pointer), and a simple
discipline. Calling a function pushes a frame (subtracts from rsp); returning
pops it (adds back). A frame holds the function's locals, saved registers, and the
return address. Because it's pure LIFO, allocation is nearly free — just move rsp —
and deallocation is automatic: when the function returns, its entire frame is gone.
- Fast. Allocating a local is one register adjustment, and the top of the stack is always hot in cache, so locals are about as cheap as memory gets.
- Automatic lifetime. You never free a local; it dies when its function returns. That's the trap too: a pointer to a local is dangling the moment you return.
- Small and fixed. The stack has a hard limit (commonly ~8 MB on Linux, ~1–8 MB on macOS threads). Blow it — deep/infinite recursion, or a huge local array — and you get a stack overflow: instant crash, no diagnostic.
The mechanics of frames (prologue/epilogue, push/pop, the call instruction) belong
to assembly & compiler output; here the
point is the policy: LIFO, automatic, bounded.
The heap: manual storage, any lifetime
When you need memory whose lifetime isn't tied to one call — it must outlive the
function that created it, or its size isn't known until runtime — you ask the heap.
malloc(n) finds a free block of n bytes and returns a pointer; free(p) returns it
to the allocator. Between those two calls the block is yours, across any number of
function returns.
- Flexible lifetime. A block lives until you free it — perfect for data structures that outlive their creator (a list you build and return).
- Manual and slower.
malloc/freerun real bookkeeping (free lists, size classes, coalescing), so each call costs far more than a stack bump, and fresh heap memory is cold in cache. - You own every consequence. Forget to
free→ a memory leak.freetwice or use afterfree→ corruption. The allocator and these failure modes are the whole subject of pointers & memory.
See both regions
This program prints an address from each region. Compile at -O0 so locals aren't
optimized into registers, then run:
// regions.c — print one address from each part of the address space.
// gcc -O0 -Wall -Wextra regions.c -o regions && ./regions
#include <stdio.h>
#include <stdlib.h>
int global_initialized = 42; // .data
int global_zero; // .bss
void show_stack_growth(int depth) {
int local; // a fresh stack slot each call
printf(" depth %d: &local = %p\n", depth, (void*)&local);
if (depth < 3) show_stack_growth(depth + 1);
}
int main(void) {
int stack_var = 7;
int *heap_var = malloc(sizeof(int));
printf("code (&main) = %p\n", (void*)main);
printf("data (&global_init) = %p\n", (void*)&global_initialized);
printf("bss (&global_zero) = %p\n", (void*)&global_zero);
printf("heap (malloc) = %p\n", (void*)heap_var);
printf("stack (&stack_var) = %p\n", (void*)&stack_var);
printf("\nstack grows DOWN as we recurse:\n");
show_stack_growth(0);
free(heap_var);
return 0;
}
A real run (macOS, x86-64) — your exact addresses differ each run because of ASLR, but the ordering and the direction hold:
code (&main) = 0x10cf69e10
data (&global_init) = 0x10cf6b000
bss (&global_zero) = 0x10cf6b004
heap (malloc) = 0x7fb520804080
stack (&stack_var) = 0x7ff7b2f95988
stack grows DOWN as we recurse:
depth 0: &local = 0x7ff7b2f95968
depth 1: &local = 0x7ff7b2f95948 <- each frame is at a LOWER address
depth 2: &local = 0x7ff7b2f95928
depth 3: &local = 0x7ff7b2f95908
Code and globals sit low and together; the heap is a big region above them; the stack is
way up high — and every recursion puts the new &local at a lower address, proving the
stack grows downward. You just saw the address-space map the table described.
Which one: a decision rule
| Use the stack when… | Use the heap when… |
|---|---|
| size is known at compile time | size is decided at runtime |
| the data dies with the function | the data must outlive the call |
| it's small (bytes–KB) | it's large (KB–GB) |
| you want zero alloc cost | you accept cost for flexibility |
Default to the stack — it's faster and frees itself. Reach for the heap only when
lifetime or size forces you to, and then make ownership (who calls free) explicit.
Failure modes & trade-offs
- Dangling pointer (returning a local).
int *f(void){ int x=0; return &x; }returns a pointer to memory that's already freed whenfreturns — using it is undefined behavior. Stack memory is auto-freed; pointers into it can't escape. - Stack overflow. Unbounded recursion or a multi-MB local array (
int buf[4'000'000]) exceeds the stack limit and crashes hard. Large or runtime-sized buffers belong on the heap. - Memory leak. Heap blocks you never
freeaccumulate until the process dies; in a long-running server that's a slow death. Everymallocneeds an owner responsible forfree. - Use-after-free / double-free. The flip side of manual heap management — corruption and exploitable bugs. Covered in depth in pointers & memory.
In practice
- Locals are stack,
mallocis heap — that's the whole tell. No keyword says "stack"; automatic variables just are stack. Onlymalloc/calloc/realloc(and friends) touch the heap. - A pointer doesn't tell you which region it points into.
int *pcan point at a local, a global, or a heap block. You must track that — especially "may Ifreethis?" (only heap blocks, exactly once). - Big or long-lived → heap; small or scoped → stack. This single instinct prevents both stack overflows and most lifetime bugs.
- This is the foundation for everything next. C's whole memory model, pointers, and allocators build directly on these two regions.
Connects to: Machine Model · The memory hierarchy · Registers & the ISA · Pointers & Memory · C from the Metal · Assembly & Compiler Output
Sources
- Bryant & O'Hallaron — Computer Systems: A Programmer's Perspective (CS:APP), ch. 3 & 9 — the stack at machine level, the process address space, and virtual memory; the spine for this note. https://csapp.cs.cmu.edu/
- Jens Gustedt — Modern C — automatic vs allocated storage duration, lifetimes, and
malloc/freedone right. https://gustedt.gitlabpages.inria.fr/modern-c/ - cppreference —
malloc/free/ storage duration — the precise C semantics of automatic vs allocated storage. https://en.cppreference.com/w/c/memory/malloc - Arpaci-Dusseau — Operating Systems: Three Easy Pieces, "Address Spaces" — why a process sees one clean virtual layout with stack and heap at opposite ends. https://pages.cs.wisc.edu/~remzi/OSTEP/
- System V AMD64 ABI — how the stack is organized at function-call boundaries (frames, alignment, the
rspdiscipline). https://gitlab.com/x86-psABIs/x86-64-ABI