The heap and the lifetime problem
Lesson 02 gave you three places memory can live — automatic (stack), static, and dynamic — and showed the stack is nearly free because a call frame is created and destroyed by simply moving one register. Lesson 03 gave you the pointer: an address is a value you can hold, copy, and dereference. This lesson opens the third storage region, the heap, and asks the one question the stack never made you answer: when memory's lifetime is not tied to a scope, who frees it, and when? That question — and the three ways of getting it wrong — is the entire motivation for the next three lessons.
new is a question with no automatic answer, and the cost model is now lifetime: a region of memory that outlives nothing in particular, owned by no one in particular, is a bug waiting for an early return.&, *, nullptr, that an address is a value you can hold and outlive the thing it points at) and Lesson 02 (the three storage durations; why the stack is cheap; the stack frame popped at scope exit).New capability: explain why a heap must exist, what
new/delete cost compared to the stack, and recognize the three classic manual-memory failures — leak, dangling pointer / use-after-free, and double free — well enough to see why discipline alone cannot prevent them. That recognition is the problem Lesson 05 solves.new and delete, and what an allocator actually does. (3) The cost: a heap allocation vs the nearly-free stack, with a rough number. (4) The central question — who frees this, and when — and the three failure modes drawn in code: leak, dangling pointer, double free, plus the invalidation preview. (5) The proof that discipline cannot win: one early return or one thrown exception between new and delete leaks. That proof is the hook for RAII.1 · Why the stack is not enough
Recall the stack from Lesson 02. When a function is called, the machine reserves a block of memory — the stack frame — for that function's locals, and when the function returns it reclaims that block by moving the stack-pointer register back. That is why stack allocation is nearly free: there is no bookkeeping, just a register adjustment. But it buys that speed with a rigid rule: every automatic object dies at the closing brace of the scope that created it. Two real needs break that rule.
The heap (also called the free store in C++) is the region that lifts both limits. It is a large pool of memory from which your program can request a block of any runtime-decided size, at any time, and that block stays valid until you explicitly release it — not when any scope ends. The power and the danger are the same sentence: its lifetime is decoupled from program structure. The stack's lifetime was free because the structure of your code decided it. On the heap, you decide, by hand.
2 · new, delete, and the allocator
The two raw operators are new (request a block, construct an object in it, return a pointer to it) and delete (destroy the object and return the block to the pool). The pointer is the only handle you have to heap memory — there is no name for it, only an address.
int* p = new int(42); // ask the allocator for sizeof(int) bytes on the heap,
// construct an int with value 42 there, return its address.
// p itself is a pointer living on the stack; *p lives on the heap.
std::cout << *p; // 42 — dereference the pointer to read the heap int
delete p; // return that block to the allocator. p is now a DANGLING pointer
// (it still holds the old address, but that memory is no longer yours).
p = nullptr; // good hygiene: a dangling pointer is a loaded gun; null it.
int n = read_count(); // runtime value
int* arr = new int[n]; // array form: n ints, contiguous, size decided at runtime
delete[] arr; // array form of delete — MUST match new[]. delete (no []) here is UB.
What is the allocator doing? Conceptually it is a librarian for one big pool of bytes. It keeps a record of which regions of the heap are free and which are handed out. On new it searches its free-list for a chunk large enough, splits it, marks it used, and returns the address. On delete it marks the chunk free again and may merge it with neighbouring free chunks so the space can be reused. None of this is magic, but none of it is free either: it is real searching, real bookkeeping, possibly a lock if threads share the heap, and occasionally a system call to the operating system to grow the pool. Contrast the stack, where "allocate" was a single register move with no bookkeeping at all.
new/delete in good modern C++ — Lesson 07's smart pointers and the STL containers (Lesson 13) call them for you, safely. We use the raw operators here precisely to expose the lifetime problem they create. Seeing the wound is what makes the cure (RAII, Lesson 05) make sense.3 · What it costs
Put a number on it, because that is the habit. A stack allocation is effectively zero cost — part of one register update that happens on function entry regardless. A heap allocation runs the allocator's logic and is on the order of tens to hundreds of nanoseconds, sometimes far more if the pool must grow or a lock contends. That is dozens to hundreds of times more expensive than a stack local, plus a second hidden cost: heap objects are scattered across the pool, so chasing pointers to them tends to miss the cache (the latency picture is Lesson 08).
| Stack (automatic) | Heap (new/delete) | |
|---|---|---|
| Allocate | ~free — adjust stack pointer | ~tens–hundreds of ns — search free-list, bookkeep |
| Free | ~free — automatic at scope exit | explicit delete; mark free, maybe coalesce |
| Lifetime | bound to the enclosing scope | until you call delete — decoupled from scope |
| Size | fixed at compile time | chosen at runtime |
| Who frees it | the language, deterministically | you — the entire problem of this lesson |
4 · The central question: who frees this, and when?
That last table row is the whole lesson. The stack never asked it; the heap asks it on every allocation, and there is no compiler, no runtime, no garbage collector to answer for you. Get the answer wrong in one of three ways and you have one of the three canonical memory bugs. Here is each, concretely.
void handle_request() {
int* buf = new int[1024]; // 4 KB on the heap
process(buf);
// ... function returns here. buf (the pointer) lived on the stack and is gone.
// The 4 KB it pointed at is still marked "used" by the allocator,
// but no pointer in the program holds its address anymore.
} // LEAK: that memory is unreachable AND unfreed — lost until the process exits.
A leak is heap memory you allocated and never released, often because the last pointer to it was overwritten or went out of scope. One leak is harmless; a leak inside a loop or a long-running server grows memory without bound until the process is killed. Nothing crashes at the moment of the bug — that is what makes leaks hard to find.int* p = new int(10);
int* q = p; // q and p now alias the SAME heap int (Lesson 03: copying a pointer copies the address)
delete p; // the int is freed; the block returns to the allocator
std::cout << *q; // USE-AFTER-FREE: q still holds the old address, but that memory is no longer ours.
// It may print 10, print garbage, or crash — undefined behavior (Lesson 19).
A dangling pointer is a pointer that still holds an address whose object has already been destroyed; reading or writing through it is a use-after-free. The danger is that it often appears to work — the freed bytes may not have been reused yet — so the bug ships and detonates later, under different timing, in production. (The same wound has a pure-stack cousin you met in Lesson 02 — int* make(){ int x=7; return &x; } returns the address of a local that dies on return — but on the heap the gap between delete and the stale read can span far more code, which is what makes it so much harder to spot.)int* p = new int(10);
int* q = p; // again, two pointers, one heap object
delete p; // first free — correct, the block goes back to the allocator
delete q; // DOUBLE FREE: q points at a block the allocator already reclaimed.
// delete corrupts the allocator's bookkeeping — undefined behavior;
// typically an immediate crash or a heap-corruption exploit primitive.
A double free is calling delete twice on the same block. It corrupts the allocator's internal free-list. Notice that double free and dangling pointer share a root cause: two pointers owned the same object and disagreed about who frees it. Ambiguous ownership is the disease; these are its symptoms.Pointer and iterator invalidation (preview). The same wound appears even when you never write delete yourself. A growing container like std::vector (Lesson 13) may move all its elements to a larger heap block when it runs out of room — and the moment it does, every pointer, reference, or iterator you were holding into the old block becomes dangling, exactly like case 2. Lesson 13 gives the precise rules; for now just register that "freed too early" is a hazard you can trip without ever typing the word delete.
5 · Why discipline cannot win — the proof
The natural reaction is: "I'll just be careful — every new gets a matching delete." That fails, and not because programmers are sloppy. It fails because control does not flow in a straight line from new to delete. Any path that leaves the function early skips the cleanup.
void process_file(const std::string& path) {
int* buffer = new int[1024]; // acquired here
if (!open(path)) {
return; // EARLY RETURN — skips the delete below. LEAK.
}
int bytes = read_into(buffer); // suppose read_into can THROW on an I/O error (Lesson 15).
// If it throws, control jumps straight out of this function,
// the stack unwinds, buffer (the pointer) is destroyed —
// but the 4 KB it pointed at is NOT freed. LEAK.
if (bytes == 0) {
return; // another early return — another path that skips cleanup. LEAK.
}
transform(buffer, bytes);
delete[] buffer; // the ONLY path that frees it is "everything went perfectly."
}
There are four ways out of this function and only one of them frees the memory. To make manual cleanup correct you would have to repeat delete[] buffer; before every return and wrap every throwing call in a handler that also deletes — and then keep that web of cleanup perfectly in sync as the function is edited for years. That is the proof: as the number of exit paths grows, the discipline approach is not merely hard, it is unmaintainable. A thrown exception (Lesson 15) is the killer case, because it can leave the function from a line you never marked as an exit at all.So the goal is not "try harder." The goal is to make cleanup happen automatically, tied to something the language already guarantees runs on every exit path — including exceptions. The stack already gave us exactly such a guarantee: a local object is destroyed deterministically at scope exit, no matter how the scope is left. Lesson 05 weaponizes that guarantee.
Common mistakes / failure modes
new[] but freed with delete (no brackets), or vice versa. Undefined behavior. The array form tracks the element count separately; mixing them corrupts cleanup.p = new int(1); p = new int(2); — the first allocation is now unreachable and leaked. Reassigning an owning pointer without first delete-ing the old target loses it.*p or calling delete p again after p was freed. Setting p = nullptr after delete turns a silent use-after-free into a loud, deterministic null-dereference crash — easier to find.Checkpoint exercise
process_file function from §5 and (a) count the exit paths and mark which ones leak — there should be one clean path and at least three leaking ones. (b) Try to "fix it by discipline": add the delete[] before every return. Now ask what happens if read_into throws — your hand-placed deletes don't run, and the leak survives. (c) Compile a tiny version with g++ -fsanitize=address,leak prog.cpp and run it: AddressSanitizer reports the use-after-free and double-free cases as errors, and LeakSanitizer prints the exact bytes leaked and the allocation stack. Watch the tool name the bug you just reasoned about. Then notice that the real fix — the one that survives exceptions — is not on this list. That fix is Lesson 05.Where this points next
We have the heap, its cost, and the central question it forces: who frees this, and when. We also have proof that answering it by hand discipline cannot survive early returns and exceptions. Lesson 05 — RAII — is the answer C++ is built around: tie the heap object's lifetime to a stack object's scope, so the language's guaranteed destructor call at scope exit becomes the delete you no longer have to remember. It runs on the happy path, on every early return, and during the stack unwinding of a thrown exception — exactly the cases that defeated discipline. The leak, the dangling pointer, and the double free do not get patched one by one; they get designed out. That is the whole second movement of this series.
new costs tens to hundreds of nanoseconds and, fatally, decouples lifetime from program structure: nothing frees heap memory but an explicit delete that you must place. Get the answer to "who frees this, and when?" wrong and you get one of three bugs — a leak (freed never), a dangling pointer / use-after-free (freed too early, still referenced), or a double free (freed twice) — the last two both rooted in two pointers owning one object. And it cannot be solved by being careful: a single early return or thrown exception (Lesson 15) between new and delete skips the delete and leaks. Manual memory management is unwinnable by discipline, which is precisely why Lesson 05 makes cleanup automatic with RAII.Interview prompts
- Why does a heap need to exist at all, given the stack is faster? (§1 — the stack binds every object's lifetime to a scope and fixes frame size at compile time; the heap is needed when an object must outlive its creating scope or its size is only known at runtime.)
- What does an allocator actually do on
newanddelete, and why isn't it free? (§2 — it manages a pool, searching a free-list for a large-enough chunk on new and marking/coalescing on delete; that's real bookkeeping, possibly a lock and a syscall, unlike the stack's single register move.) - Roughly how much more expensive is a heap allocation than a stack local, and why? (§3 — tens to hundreds of ns vs effectively zero, because the allocator runs real search/bookkeeping logic and heap objects scatter and miss the cache, whereas the stack is one pointer adjustment.)
- Define leak, dangling pointer, and double free, and name the cause the last two share. (§4 — leak = allocated, never freed; dangling = pointer to an already-destroyed object (use-after-free if dereferenced); double free = delete called twice; the latter two both stem from two pointers owning one object with unclear ownership.)
- Why can't careful matching of every
newwith adeleteprevent leaks? (§5 — control doesn't flow straight from new to delete; any early return or thrown exception between them skips the delete, and the number of such exit paths makes hand-placed cleanup unmaintainable.) - How can you get a dangling pointer without ever writing
delete? (§4 — a container like vector can reallocate to a larger heap block as it grows, invalidating every pointer, reference, and iterator into the old block — Lesson 13.) - What property of the stack does the solution to all this exploit? (§5 / Lesson 05 — a local object's destructor is guaranteed to run deterministically at scope exit on every path including exceptions; RAII ties the heap object's release to that guarantee.)