People talk about “the stack” and “the heap” like they are physical objects you can point at. They are not. They are two regions of the same virtual address space, managed in completely different ways. I spent years writing C before I really internalized the difference, and once I did, a lot of confusing bugs suddenly made sense.
The stack is a region, not a data structure
When your program starts, the operating system hands each thread a chunk of contiguous memory called the stack. It grows in one direction, usually downward toward lower addresses on x86 and ARM. Every time you call a function, the CPU pushes a stack frame: the return address, saved registers, and room for local variables. When the function returns, that frame is gone instantly. No bookkeeping, no search, just a single register adjustment.
That is why stack allocation is fast. There is a register, the stack pointer, and “allocating” 64 bytes means subtracting 64 from it. Freeing means adding it back. The cost is effectively zero.
The catch is lifetime. A stack variable lives exactly as long as the function call that created it. Return a pointer to a local and you are pointing at memory that the next function call will scribble over.
// This is a bug. The buffer dies when the function returns.
char *make_greeting(void) {
char buffer[32];
snprintf(buffer, sizeof buffer, "hello");
return buffer; // dangling pointer
}
The heap is for things that outlive a frame
The heap is the rest of your usable address space, and it is managed by an allocator (malloc and friends) rather than by the CPU. When you ask for memory you get a block that stays valid until you explicitly free it. That flexibility is the whole point, and it is also where the work hides. The allocator has to track which blocks are free, find one big enough, and hand it back. I wrote a whole post on writing a simple memory allocator because that machinery is worth understanding directly.
char *make_greeting(void) {
char *buffer = malloc(32); // lives on the heap
if (!buffer) return NULL;
snprintf(buffer, 32, "hello");
return buffer; // valid, but the caller now owns it
}
The tradeoffs you actually feel
- Speed: stack allocation is a pointer bump. Heap allocation walks data structures and may call into the kernel. The gap is large.
- Lifetime: stack memory is tied to scope. Heap memory lives until you free it, which means you have to remember to free it.
- Size: stacks are small, often 1 to 8 MB. Try to put a 10 MB array on the stack and you get a stack overflow. Big data goes on the heap.
- Locality: stack memory is hot. It was just touched, so it is almost always in cache. Heap memory can be scattered, which matters more than people expect.
Why this connects to performance
The locality point is the one that bites in real systems. A pointer to the heap is a value, and following that pointer is a value too. Where it physically lands decides whether your CPU stalls. I dig into that in data-oriented design and CPU caches, but the short version is that scattering your data across heap allocations can be slower than the algorithm would suggest, purely because of cache misses.
What the addresses look like
If you print the address of a local variable and the address of a malloc result in the same program, the difference is stark. Stack addresses tend to be high and close together, because everything in the current call chain sits in one tight region. Heap addresses sit lower and spread out as the heap grows upward to meet the stack growing downward. They are marching toward each other through the same address space, and in the old days a program that used too much of both would have them collide. Virtual memory and guard pages make that collision a clean crash today instead of silent corruption.
One more thing worth knowing: allocation on the stack is not just fast, it is also automatically aligned and laid out by the compiler, which knows every local’s size up front. The allocator has to compute alignment and padding at runtime. That extra work is part of why the heap costs more per allocation, on top of the bookkeeping.
A mental model that holds up
Here is how I think about it now. The stack is a scratchpad the CPU manages for you, perfect for short-lived, known-size values. The heap is a warehouse you manage yourself, for anything whose size or lifetime you cannot pin down at compile time. Most bugs in C come from confusing the two: returning stack pointers, freeing heap memory twice, or forgetting to free it at all.
The reason languages like Rust feel safe is that they encode these rules into the type system so the compiler catches the mistakes. If you want to see how that works without a garbage collector, read memory safety with Rust ownership and borrowing. But you cannot really appreciate what Rust is protecting you from until you have felt the sharp edges of the stack and the heap yourself.