Stack, Heap, Lifetimes, and Scope
Where your variables actually live, when they die, and why both questions are the same question
Try it. Press call f() to push a new frame. Declare an int, then new int(…), then exit f() — watch the heap block turn orange (“leaked”) because the pointer that referenced it died with the frame. Then delete before exiting and see it freed properly. The widget is a toy runtime: the rules it enforces are the rules of the C++ memory model.
Why it matters
The single hardest thing about C++ for newcomers from garbage-collected languages isn’t the syntax. It’s the question:
When does this object get destroyed?
In Python, “whenever, eventually, the GC decides.” In C++, the answer is exact, knowable, and burned into the source code at the place the object is declared. Reading C++ fluently means always knowing the lifetime of every name in scope. This lesson teaches you to see it.
Two regions, one mental model
A running C++ program owns two interesting regions of memory:
- The stack. Fast, automatic, finite (typically 1–8 MB total). Each function call pushes a frame containing that call’s local variables. Each return pops the frame. The compiler decides everything.
- The heap. Slower, manual, effectively unbounded (gigabytes). You ask
the heap for storage with
new(or, much better, a smart pointer factory), and it lives until you hand it back.
The two regions exist for the same reason your laptop has both RAM and SSD: one is fast and bounded, the other is slow and roomy. The trade-off is the curriculum of this whole phase.
A stack frame, drawn
Take the simplest possible function. What does the stack look like the moment the compiler is on the last line?
int main() {
int x = 5;
int y = 42;
// ← we are here
return 0;
}
The frame is a contiguous block. x and y sit next to each other in
memory at predictable offsets from the frame pointer. There is no
allocator involved. There is no delete to write. When main returns,
the whole frame is gone in one stack-pointer adjustment.
This is what “stack allocation” buys you. It is, by orders of magnitude, the cheapest memory in your program.
Scope is lifetime (for automatic storage)
The C++ rule is hard but it is exactly one rule:
An automatic variable lives from the point of its declaration to the closing brace of the enclosing block. Then it is destroyed.
Watch:
#include <iostream>
struct Sentinel {
const char* name;
Sentinel(const char* n) : name(n) {
std::cout << " born: " << name << "\n";
}
~Sentinel() {
std::cout << " die: " << name << "\n";
}
};
void demo() {
Sentinel a{"a (outer)"};
{
Sentinel b{"b (inner)"};
std::cout << "inside inner block\n";
} // b dies here, b's destructor runs
std::cout << "back to outer\n";
} // a dies here
int main() {
demo();
std::cout << "back to main\n";
return 0;
}
A Sentinel logs its own birth and death. Run this and you see lifetimes line up with braces, exactly.
born: a (outer)
born: b (inner)
inside inner block
die: b (inner)
back to outer
die: a (outer)
back to main Or run locally
g++ -std=c++23 -O2 snippet.cpp && ./a.out Note the destructors fire in reverse order of construction — b was
born after a, so b dies first. The standard guarantees this. It is what
makes the next lesson’s smart pointers (and std::lock_guard, and
std::fstream’s auto-close, and every other RAII type) work.
Heap allocation: when scope isn’t enough
Some objects need to outlive the function that created them. Some objects
are too big for the stack. Some objects don’t know their size until
runtime. For all of these, you allocate on the heap with new (just for
this lesson — in real code you use the smart pointers from lesson 05).
int* p = new int(42); // p lives on the stack, *p lives on the heap
// … use *p …
delete p; // hand the heap memory back
Two objects, two lifetimes, one mental load:
- The pointer
pis an automatic variable. It dies whenmainreturns, full stop. - The int at
*plives until you calldelete p. If you forget, that’s a memory leak. If you calldeletetwice, that’s undefined behavior and your program may crash, corrupt unrelated memory, or appear to work fine until a customer notices.
This asymmetry is the entire reason RAII (lesson 05) exists.
The dangling pointer: same bug, two flavors
The cardinal sin in C-style memory management is referring to memory that no longer belongs to you.
Flavor 1: returning the address of a local
int* bad() {
int local = 42;
return &local; // local dies here. caller gets a pointer to nothing.
}
The compiler will usually warn you. Modern compilers are good at catching this exact pattern. Always treat the warning as an error.
Flavor 2: use-after-free
int* p = new int(42);
delete p; // heap memory returned to the allocator
std::cout << *p; // reading from a slot that may now hold anything
The pointer value p is unchanged. The bytes it points at are now owned
by the allocator, which is free to hand them to the next new. Reading
*p after delete p gives you whatever happens to be there. Sometimes
you’ll get 42 for a while and convince yourself the bug is elsewhere. The
universe is patient.
Heap is for: outlives-the-function, doesn’t-fit, size-unknown
The rule of thumb for stack vs heap in modern C++:
| You want… | Use |
|---|---|
| A small, function-local value with a known lifetime | Stack. Just declare it. |
| A big buffer (≥ a few KB) | Heap. Stacks are small. |
| An object that outlives the function that created it | Heap, owned by a smart pointer. |
| A collection whose size isn’t known until runtime | Heap, but use std::vector and let it manage the allocation. |
| Something polymorphic (pointer to base class) | Heap, owned by std::unique_ptr<Base>. |
The pattern: prefer the stack. When you can’t, prefer a container (like
std::vector) that manages the heap for you. When you can’t do that
either, use a smart pointer. Calling new and delete directly should be
rare in modern C++ — we’ll cover when in lesson 05.
std::vector: the heap, with manners
A std::vector<int> v; is an object that lives on the stack but owns a
heap-allocated array of ints. When v goes out of scope, its
destructor releases the array. You get the heap’s flexibility without
the bookkeeping.
#include <iostream>
#include <vector>
int main() {
std::vector<int> v; // empty, on the stack; no heap allocation yet
for (int i = 0; i < 5; i++) v.push_back(i * i);
std::cout << "v has " << v.size() << " elements (capacity " << v.capacity() << ")\n";
for (int x : v) std::cout << x << ' ';
std::cout << "\n";
return 0;
}
The vector owns the heap allocation. When the vector dies, the array dies with it. No `delete` to forget.
v has 5 elements (capacity 8)
0 1 4 9 16 Or run locally
g++ -std=c++23 -O2 snippet.cpp && ./a.out This is the C++ idiom: use containers, not raw new/delete. The
container’s destructor knows what to free and when. You can’t forget
because the language fires the destructor for you.
Stack overflow: the price of “fast and bounded”
The stack is fast, but it’s also small. Default thread stacks are
typically 1–8 MB on Linux, 1 MB on Windows. Allocate a
char buffer[2'000'000]; on the stack and your program may crash before
main runs. Recursive functions that don’t bottom out the same way.
#include <iostream>
void recurse(int depth) {
char ballast[1024]; // 1 KB per frame, to make this visible
ballast[0] = (char)depth;
if (depth % 5000 == 0) std::cout << "depth " << depth << "\n";
recurse(depth + 1); // no base case — will exhaust the stack
}
int main() {
std::cout << "about to overflow…\n";
recurse(0); // your shell will probably show signal 11 / SIGSEGV
return 0;
}
Don't ship this. Do run it once so the failure mode is in your bones.
about to overflow…
depth 5000
depth 10000
depth 15000
…
Segmentation fault (core dumped)
# (the kernel kills the process with SIGSEGV once the stack guard page is hit) Or run locally
g++ -std=c++23 -O2 snippet.cpp && ./a.out The OS catches stack overflow as a segfault. There is no Python-style
RecursionError. The fix: keep automatic objects small, prefer iteration,
and put big buffers on the heap.
Key takeaways
- The stack holds automatic variables; their lifetime is exactly their scope. Cheap, fast, small.
- The heap holds dynamic allocations; lifetime is whatever you make it. Slower, manual, bigger.
- Destructors fire in reverse order of construction, automatically, when scope ends. This is the foundation of RAII.
- Dangling pointers come in two flavors: returning the address of a local, and use-after-free. Both have the same shape — a pointer into storage that’s no longer yours.
- Prefer the stack, then containers that manage the heap for you,
then smart pointers. Raw
newanddeleteshould be rare.
What’s next
Lesson 04 dives into pointers and references in detail: when each is the
right tool, what const T* vs T* const actually means, and why
nullptr is the only spelling worth using. Then lesson 05 introduces the
two smart pointers that make manual new/delete almost obsolete.