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:

  1. Logic bug? → not here
  2. Leak? → Valgrind says no
  3. Stack overflow — stack limit exceeded. In GDB, bt shows thousands of identical fibonacci frames, 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
RegionRoleLifetime
StackLocals, args, return addressesUntil scope returns
Heapnew / mallocUntil freed or process exit
Data/BSSGlobals, static variablesWhole 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:

  1. Fast—just move the stack pointer
  2. Automatic—destructors run on scope exit, even when an exception propagates
  3. Cache-friendly—the top of the stack is almost always in L1 cache

Disadvantages:

  1. Small—don’t put megabytes as int huge[1000000] local
  2. Lifetime tied to scope—returning the address of a local → dangling pointer
  3. 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

  1. Stack overflow — huge locals / deep recursion
  2. 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.
  3. delete on stack memory — undefined; glibc typically aborts with “free(): invalid pointer”
  4. Double free / use-after-free
  5. Leak — new without delete on 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/delete of same-sized objects
  • Stack allocators / arenas for frame-scoped bump allocation, freed all at once (C++17 std::pmr::monotonic_buffer_resource is 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.


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

References