Stack vs Heap in C++: Why Deep Recursion and Big Locals Overflow, and When to Allocate
Introduction: Crashed by stack overflow
Why the process died — recursion trap
You implemented Fibonacci recursively. n = 10 worked; n = 100000 killed the process with no helpful message. The stack is a small, fast region—like a narrow workbench: deep recursion stacks many frames, each holding locals, until the bench overflows.
The demo below reserves ~4KB per call (int cache[1000]). The first call chain fibonacci(n-1) descends 100000 levels before any call returns, so the program needs roughly 100000 × 4KB ≈ 400MB of stack at its deepest point, far beyond any default limit → stack overflow. The second branch, fibonacci(n-2), makes the running time exponential but does not add depth; depth, not the number of calls, is what consumes stack. (The cache array is unused in logic—it illustrates per-frame stack cost.)
// g++ -std=c++17 -o fib fib.cpp && ./fib
#include <iostream>
int fibonacci(int n) {
int cache[1000]; // ~4KB per frame
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
int main() {
std::cout << fibonacci(100000); // crashes — stack overflow
return 0;
}
Build it without optimization to see the crash reliably. With -O2 the compiler may remove the unused array, and the frames shrink to a few dozen bytes; the program then runs for a very long time instead of crashing, which is a different symptom of the same design problem.
Debugging path:
- Logic bug? → not here
- Leak? → Valgrind says no
- Stack overflow — stack limit exceeded. In GDB,
btshows thousands of identicalfibonacciframes, which is the tell-tale sign.
Takeaway: know stack vs heap; huge locals and deep recursion are dangerous on the default stack.
This article explains:
- How stack and heap behave
- How to avoid stack overflow
- When to use stack vs heap
- How layout fits later smart pointers and RAII
Previous: Compilation process
C++ process memory (simplified)
The OS maps text (code), data/BSS (globals), heap (grows up), and stack (grows down). In a modern 64-bit process they do not actually collide: the address space is huge, shared libraries and mmap regions sit between them, and the stack has a fixed maximum with an unmapped guard page below it. Touching that guard page is what turns a stack overflow into a segmentation fault instead of silent corruption.
flowchart TB
subgraph addr["Address direction (low → high)"]
direction TB
T[Text code]
D[Global/static Data, BSS]
H[Heap ↑]
free[Free space]
S[Stack ↓]
end
T --> D --> H --> free --> S
style H fill:#e1f5fe
style S fill:#fff3e0
| Region | Role | Lifetime |
|---|---|---|
| Stack | Locals, args, return addresses | Until scope returns |
| Heap | new / malloc | Until freed or process exit |
| Data/BSS | Globals, static variables | Whole program |
Why stacks are small: every thread needs its own stack, and the address range for it is reserved up front. Typical defaults are 8MB for the main thread on Linux (see ulimit -s) and 1MB on Windows (set by the linker’s /STACK option). Secondary threads get whatever the threading library chooses unless you configure it, so code that is fine on the main thread can overflow on a worker.
Stack memory
Fast, automatic, limited. Each function call pushes a stack frame (locals, saved registers, return address). Entering a function adjusts the stack pointer by the frame size in one instruction; leaving it moves the pointer back. There is no bookkeeping about individual objects, which is exactly why it is cheap and why the lifetime rules are rigid.
Advantages:
- Fast—just move the stack pointer
- Automatic—destructors run on scope exit, even when an exception propagates
- Cache-friendly—the top of the stack is almost always in L1 cache
Disadvantages:
- Small—don’t put megabytes as
int huge[1000000]local - Lifetime tied to scope—returning the address of a local → dangling pointer
- Size fixed at compile time—variable-length arrays are a compiler extension in C++, not standard
Stack overflow examples
Huge local array → use std::vector on the heap. std::array<int, 1'000'000> has the same problem as a C array, because std::array stores its elements inline.
Deep recursion with big locals → reduce depth, move arrays to heap, or iterate. Recursive tree or graph traversal on user-supplied data is the real-world version of this bug: a degenerate input (a linked-list-shaped tree, a very long path) produces depth equal to the input size. Converting to an explicit std::stack or std::vector of pending nodes moves that depth onto the heap, where it is bounded only by available memory.
Mitigation (last resort):
ulimit -s 16384 # Linux: 16MB (example)
ulimit -s affects only processes started from that shell afterwards, and only the main thread. For std::thread there is no portable stack-size option; you need the platform API (pthread_attr_setstacksize, or Boost.Thread attributes).
Root fix: don’t rely on giant stacks—heap or iteration.
The situation I have seen most often is not recursion at all but a buffer that grew over time: a char buf[4096] becomes char buf[65536] during a feature change, the function is called from a worker thread with a smaller stack, and the crash shows up only in one deployment. Because the program dies with a plain segfault in an unrelated-looking function, the first instinct is to look for a bad pointer. Checking the frame size (-Wstack-usage=N or -fstack-usage in GCC reports it per function) settles it quickly.
Heap memory
Flexible size and lifetime; you (or smart pointers) must free it.
Pros: large allocations, outliving scope, runtime-sized and polymorphic objects Cons: slower, fragmentation risk, manual correctness without RAII
new calls operator new, which asks the allocator (glibc malloc, jemalloc, the Windows heap, and so on) for a block of suitable size, then runs the constructor. The allocator keeps free lists, may lock or use per-thread caches, and for large requests may go directly to the OS with mmap. On failure, new throws std::bad_alloc rather than returning null. Every allocation must be matched by exactly one release of the same kind: new/delete, new[]/delete[], malloc/free. Mixing them is undefined behavior, which is one of the reasons modern C++ code rarely writes delete by hand.
Stack vs heap performance
Stack “allocation” is essentially free; a heap allocation runs allocator code, and the cost varies with allocator, size and thread contention. The practical consequence is simple: don’t allocate and free on the heap in every iteration of a hot loop if you can avoid it. Reuse a buffer declared outside the loop (a std::vector keeps its capacity after clear()), call reserve() when you know the final size, and measure with a profiler before reaching for custom allocators.
Access speed, once allocated, is the same for both regions: memory is memory. Where heap data is slower in practice, the cause is usually layout (objects scattered across the heap through pointers) rather than the heap itself.
Selection guide
- Small, local, short-lived, size known at compile time → stack
- Large, shared, runtime-sized, polymorphic, or must outlive the function → heap, held by
vector/unique_ptr
A useful rule: if you are not sure the size is small (a few kilobytes at most) and fixed, use a container. The container object sits on the stack and gives you automatic cleanup, while the data goes to the heap.
Patterns
- unique_ptr for sole ownership
- shared_ptr when ownership is genuinely shared (watch cycles → weak_ptr)
- RAII wrappers for buffers and files
Common errors
- Stack overflow — huge locals / deep recursion
- Dangling pointers — address of stack local returned. GCC and Clang warn with “address of local variable returned” (
-Wreturn-local-addr/-Wreturn-stack-address), but only for direct cases. deleteon stack memory — undefined; glibc typically aborts with “free(): invalid pointer”- Double free / use-after-free
- Leak —
newwithoutdeleteon all paths, including exception paths
Debugging tips
- Valgrind / ASan (
-fsanitize=address) for heap corruption and leaks. ASan reports a stack overflow as “stack-overflow on address …” with the recursive frames, which is clearer than a bare segfault. - GDB + core for post-mortem stack traces
- ulimit -s to inspect the stack limit
Production patterns
- Object pools to cut repeated
new/deleteof same-sized objects - Stack allocators / arenas for frame-scoped bump allocation, freed all at once (C++17
std::pmr::monotonic_buffer_resourceis a standard version) - Default to unique_ptr, shared_ptr only when needed
FAQ
Biggest difference stack vs heap?
A. Stack memory is managed automatically by scope, small and fast; heap memory is flexible and large but must be released explicitly, ideally by a smart pointer or container.
How to prevent stack overflow?
A. Avoid huge stack arrays; bound recursion depth or convert to iteration with an explicit stack; use heap containers.
Stack vs heap speed?
A. Stack allocation is a pointer adjustment; heap allocation runs allocator code whose cost depends on the allocator and workload. Measure your own hot path rather than relying on generic ratios.
Why not return address of stack variable?
A. After return the frame is gone and will be reused by the next call—dangling pointer → UB. It often “works” in small tests because nothing has overwritten the memory yet.
Related Articles
- Memory leaks
- Smart pointers
- Rust Memory Safety: Ownership, Borrowing, Lifetimes, unsafe
- Rust Ownership | Ownership, Borrowing, and Lifetimes
- C++ Memory Management: new/delete, Stack vs Heap, and RAII
Stack or heap: the short rule
- Stack: fast, automatic, limited
- Heap: flexible, must manage (prefer smart pointers)
- Default: small locals on stack; large data on heap
- Watch recursion and large arrays on the stack
Next: Memory leaks #6-2