Stack vs Heap Allocation

The memory lesson in Level 0 gave you the map: a stack of call frames that grows and shrinks with function calls, and a heap for values that outlive any one call. Every language uses both, but most decide for you, numbers here, objects there, collector cleaning up behind. C++ puts the decision in your hands on every declaration, and the standard gives the two choices precise names: automatic storage duration for stack objects and dynamic storage duration for heap objects. This lesson is about what each choice buys and costs.

Automatic storage: the stack does the bookkeeping

Declare an object as a plain local variable and it has automatic storage: it is created when execution reaches the declaration and destroyed, automatically, when its enclosing scope ends. No cleanup call, no collector, the closing brace is the cleanup. Allocation is close to free because a stack frame is carved out by moving one pointer, and locals within it are fixed offsets the compiler computed in advance. Deallocation is the same pointer moving back. This is why the guideline default is unambiguous: prefer scoped objects, and do not heap-allocate unnecessarily.

void round() {
  int score = 0;              // born here, on the stack
  std::string name = "Nova";  // this object too
  score += 25;
  std::cout << name << " " << score << "\n";
}                             // scope ends: both destroyed, in reverse order
// after round() returns, its whole frame is gone: cost, one pointer move

Think of the stack as a stack of trays in a cafeteria. Calling a function takes a fresh tray and everything the function needs sits on that one tray. Returning puts the tray back, and everything on it goes with it, always the reverse of the order it was picked up, no decisions needed. The heap, next, is more like renting storage lockers: you ask for space, you get a claim ticket, and the locker stays rented until someone with the ticket cancels it.

Dynamic storage: you take over the bookkeeping

Sometimes the tray is the wrong tool: the object is too big for a frame, its size is unknown until runtime, or it must outlive the function that created it. The new expression allocates an object on the heap and hands back a pointer, and the object then lives until somebody explicitly executes delete on that pointer. Power and price arrive together. The heap allocator must search its records for a free block, mark it used, and later reclaim it, real work, involving global bookkeeping, compared to the stack's single pointer move. And the lifetime is now yours to manage, which creates two famous failure modes.

// inside int main(); raw new/delete shown for the mechanics.
// (The next lesson replaces this pattern with RAII.)
int* hp = new int(100);   // allocated on the heap, survives scope exits
*hp -= 35;
std::cout << *hp << "\n"; // 65
delete hp;                // manual end of life: the heap block is reclaimed
// forgetting delete -> leak. delete-ing twice -> undefined behavior.
// using *hp after delete -> reading memory that is no longer yours.

The two failure modes, named

Forget to delete and the block stays reserved forever: a memory leak. One leak is invisible; a leak inside a loop in a server that runs for weeks is an outage. The reverse mistake is worse. Free the object while a pointer to it still exists somewhere, then use that pointer, and you have a dangling pointer dereference: the address still looks valid, but the allocator may have handed the memory to someone else, so reads return garbage and writes corrupt an unrelated object. The pointers lesson called dereferencing an invalid pointer undefined behavior; this is the main factory that produces invalid pointers. Watch both bugs happen, in miniature, below.

There is a third territory worth naming: static storage duration, for globals and static locals, which live for the entire run of the program. And one more stack subtlety: returning the address of a local is the classic instant dangling pointer, because the frame it lived in is recycled by the very act of returning. Compilers warn about the simple cases; the sneaky ones travel through a saved pointer or a captured reference and detonate later. The rule that prevents all of them is lifetime discipline: never let a pointer or reference outlive the object it names.

Choosing, and the cost intuition

The decision procedure in practice is short. Default to the stack: fixed size, scope-bound lifetime, near-zero cost, immune to leaks by construction. Reach for the heap only when the object's size is decided at runtime or its lifetime must escape the creating scope, and even then, as the arrays lesson showed, you usually reach through a container like std::vector, which heap-allocates its elements internally while itself living happily on the stack. That sentence is the whole modern C++ memory story in one line: stack objects managing heap resources, which is the exact idea the next lesson gives a name.

Stack, heap, and the reverse engineer

This division is load-bearing in Level 5. When an attacker dumps a process, stack frames are transient, but the heap is where long-lived treasure sits: decrypted buffers, loaded configuration, decoded strings, exactly the values a program had to materialize to use. Tools that hunt for a game's health value scan the heap; tools that trace calls read the stack. A defender who knows which territory a secret occupies, and for how long, can reason about what a memory snapshot at any given instant would actually contain, which is the honest way to think about the run-and-dump ceiling.

Stack allocation is a pointer move and cleanup is the closing brace: fast, automatic, leak-proof. Heap allocation buys runtime-sized, scope-escaping lifetime at the price of allocator work and manual bookkeeping, and mismanaged bookkeeping is the source of leaks and dangling pointers. Default to the stack; take the heap only when size or lifetime forces it, and prefer to take it through a managing object.

Frequently asked questions

How do garbage-collected languages relate to this picture?

Lua, JavaScript, Python, Java, and Go make nearly everything heap-allocated and replace the delete obligation with a collector that finds unreachable objects and reclaims them. You trade the dangling-pointer and leak bug classes for collector pauses and less predictable reclamation timing. C++ chose the other branch: deterministic, programmer-visible lifetimes, with the RAII discipline of the next lesson recovering most of the safety automatically.

Is the stack ever the wrong place for a small object?

Yes, whenever its lifetime must escape. A callback that fires after the creating function returns, a node spliced into a long-lived list, a buffer handed to another thread: all must outlive their creating scope, so they cannot be automatic locals no matter how small they are. Lifetime, not size, is the first question.

Keep learning

  • Where Values Live: Memory, Stack, and Heap
  • Pointers and References
  • RAII and Smart Pointers
  • Reading Memory: Values Have to Exist Somewhere
  • Java & JAR Obfuscator
  • Go Obfuscator
  • Lua Obfuscator

All lessons