A memory leak in C++ is an allocation on the heap that nothing in the program can reach anymore, and that nothing ever frees. The earlier chapter on dangling pointers introduced the basic shape of a leak. This chapter goes further: the common patterns that cause leaks in code, how to reason about ownership to stop creating them, and the tools that find the ones missed during development.
Think about the heap as a wall of mailboxes. Every new rents a mailbox, and the only way to find that mailbox again is the pointer the allocator handed back. Lose the pointer, and the mailbox can never be returned. The contents stay there until the process dies.
A leak isn't a crash. The program runs, prints the right thing, exits cleanly, and the operating system reclaims everything at exit. On a short-lived command-line tool, the leak may go unnoticed. On a server that handles a million orders a day, a few hundred bytes leaked per order is a few hundred megabytes a day, and after a week the machine starts swapping or the kernel kills the process for using too much memory.
The reason leaks are easy to write in raw-pointer C++ is that the compiler doesn't track ownership. new Order(...) returns an Order* and the compiler stops caring. Whether delete gets called on that pointer is the programmer's problem.
Three calls. Three leaked ints. The program prints the expected output and exits cleanly. Nothing in the output reveals the leak. That's the recurring theme of this chapter: leaks are silent.
Three questions are worth asking every time new is written. Who owns this allocation? When is it freed? What if an exception is thrown before the delete? If those three can't be answered quickly, a leak is almost certainly about to be written.
A snapshot of the running process. The stack holds local variables. The heap holds objects allocated with new. A pointer variable is "live" when it sits in some currently-running stack frame or in a reachable object. A heap block is reachable when at least one live pointer holds its address.
The stack frame is healthy. The pointer order is nullptr because the function that allocated those Order objects has long since returned, and the pointer variable it used to track them is gone. The three heap blocks are still allocated. The allocator considers them in-use. But nothing in the program holds their addresses anymore, so no future code can ever free them. They are leaked.
That's the precise definition of a leak. Allocator says "in use", program has no way to reach it. The allocator's bookkeeping and the program's reality have drifted apart.
new and deleteA function allocates, does some work, and then decides to bail out for a reason unrelated to the allocation. The delete sits at the bottom of the function and never runs.
The first call works. The second call hits the early return and leaks the CartItem. The function has two exit paths, and only one of them frees the allocation. Three exit paths with one delete is just as bad.
The literal fix is to copy the delete to every exit path. Two paths means two delete lines. That works for the example above. It doesn't scale. A function with five validation checks and three error paths grows five duplicate delete lines, and the next person to add a sixth check forgets one. This is the problem RAII was invented to solve.
new and deleteThe same problem in a different disguise. An exception thrown between the new and the delete skips every line up to the catch handler, which means it skips the delete too.
new Order(...) succeeds. computeTax throws. The runtime starts unwinding the stack and never reaches delete o. The Order is leaked. The pointer variable o is destroyed during unwinding, but destroying a raw pointer doesn't free what it points at. The handler in main runs, prints the failure, and the program exits with one leaked allocation behind it.
A try/catch inside finalizeOrder that calls delete and rethrows papers over this:
It works. It's also verbose, brittle, and easy to forget on the next function. Every allocation needs its own try/catch wrapper. Adding a second new to the function requires nesting the wrappers correctly to avoid leaking the first one if the second one throws. This is the textbook case for using std::unique_ptr (covered in later chapters), which deletes the allocation automatically during stack unwinding without any extra code.
A pointer holds the only reference to a heap block. Overwriting that pointer with something else makes the block unreachable. This happens more often than expected, especially with reassignment inside loops.
The first int(50) is leaked. The line stock = new int(100); doesn't free the previous allocation; it makes stock point somewhere else. The address that used to be in stock is gone, and the allocator still considers that int rented. The delete stock at the end only frees the second allocation.
The fix in raw-pointer code is to free before overwriting.
This is the same idea as Pattern 1, compressed into a single assignment. The general rule: any time an owning pointer points somewhere new, the old target must be freed first.
A std::vector<Order*> is the classic shape. The vector manages its own buffer of pointers, but it has no idea what those pointers point at. When the vector is destroyed, it frees the buffer holding the pointers; it doesn't free the Order objects on the heap that each pointer references.
Five leaked Order objects. The vector's destructor runs at the end of main, frees its internal buffer of five Order* values, and that's the end of the vector's job. The five heap blocks that each pointer referenced are still allocated, and now nothing in the program can reach them.
The literal fix is to walk the vector and delete each element before the vector goes out of scope.
Easy to forget. Easy to skip on an early return path. Easy to break the moment an exception appears anywhere in the loop body. The modern C++ answer is std::vector<std::unique_ptr<Order>>, where the vector's destructor automatically destroys each element, which in turn frees the heap allocation. That's covered in the smart pointer chapters.
A std::vector<T*> is one indirection per access plus a separate heap allocation per element. A std::vector<T> stores the objects directly in the vector's buffer, which is usually faster and never leaks. Prefer storing objects by value in containers unless there's a real reason for the indirection.
delete Form for an Arraynew[] and delete[] are a matched pair. Using plain delete on an array allocated with new[] is undefined behavior, and on most implementations it frees only the first element while leaking the rest of the array.
The program prints the expected output. The bug is invisible at runtime without a sanitizer. The allocator's bookkeeping for an array allocation is different from a single-object allocation, and freeing with the wrong form leaves the allocator's tables in an inconsistent state. Sometimes the next allocation crashes. Sometimes it doesn't. Either way it's a bug. The fix is one character: delete[] prices;.
Brief teaser, full coverage in the polymorphism chapters. Deleting an object through a base-class pointer when the base class doesn't have a virtual destructor destroys only the base part of the object. The derived part, including any heap resources it owns, leaks.
The fix is to make Base's destructor virtual. The polymorphism section explains why, what a vtable is, and what exactly goes wrong at the bytecode level. For this chapter, the point is that "missing virtual destructor on a polymorphic base class" is one of the most common ways to leak from object-oriented C++ code.
The unifying idea behind every pattern above is ownership. Who is responsible for freeing this allocation? When? What happens if control leaves through an unexpected path?
In well-written raw-pointer C++ code, every heap allocation has exactly one owner at any given time. The owner is the variable, object, or container whose job is to call delete at the right moment. Everyone else who holds the same pointer is an _observer_, looking but not owning, and explicitly not responsible for freeing.
Three concrete questions to ask of every new:
delete. If there's no name, there's no owner. That's a leak waiting to happen.new and the delete. If any can throw, the delete won't run. Eliminate the throw, wrap it in try/catch, or use a smart pointer (next chapters).These three questions are not optional. A reader who can't answer them is going to write a bug into the code base the first time they touch it.
| Ownership Pattern | Who frees | Typical bug |
|---|---|---|
| One pointer owns, no aliases | The owner | Early-return leak |
| One owner, several observers | The owner | Use-after-free if observer outlives owner |
| Shared ownership (multiple parties) | Last one to release | Double-delete if all parties try to free |
| No clear owner | Nobody | Leak |
The "no clear owner" row is the recurring source of bugs. Code that allocates without a designated owner is broken at the design level, not just the line level.
A function that loads a customer's recent orders, computes a total, and prints a summary. It has leaks on the success path, leaks on the failure path, and a bonus leak if the exception path triggers.
The output looks fine. Three Order objects leak on the success path. The vector loaded goes out of scope at the end of sumRecentOrders, its buffer of three pointers is freed, and the three heap-allocated Order objects are abandoned.
A second problem hides on the failure path. If loadOrder returns nullptr mid-loop, the throw skips past the rest of the function, and every Order already in loaded is also leaked. Two leak shapes, one function.
The fixed version using raw pointers and explicit cleanup. This is what defensive raw-pointer code looks like before smart pointers enter the toolkit.
Same output. No leaks. The cleanup loop appears twice, once in the catch-and-rethrow path and once on the normal return path. Adding a second early return inside the try block requires a third copy of the cleanup loop. This is the kind of code RAII makes obsolete. A std::vector<std::unique_ptr<Order>> replaces both the catch block and the manual cleanup loop with nothing at all. The vector's destructor walks the elements, the smart pointer's destructor frees the heap object, and the entire function shrinks by ten lines.
For now, the takeaway is structural. Raw-pointer cleanup is verbose, fragile, and easy to miss. The fact that the fixed function above is much longer than the broken one signals that a better tool is needed.
Leaks happen even with care, code review, and experience. The good news is that several mature tools find them automatically. Use them in test runs and CI, not only when something goes wrong.
AddressSanitizer (ASan) is a compiler instrumentation built into both g++ and clang++. Compile with -fsanitize=address -g and the resulting binary watches every heap allocation, every free, and every memory access. At program exit it also reports any allocations that were never freed.
Running the leaky recordOrder example from earlier under ASan produces a report shaped like this:
Three details. ASan reports the exact line where the leaked allocation was made (leak.cpp:4). It reports how many bytes and how many distinct allocations leaked. It groups identical stack traces, so a leak that happens a million times is reported once with a count, not a million times.
ASan also catches use-after-free, double-free, heap-buffer-overflow, and stack-use-after-return at the moment they happen, with a precise stack trace pointing at both the bad access and the original allocation. The runtime cost is roughly a 2x slowdown and a few times more memory usage, which is fine for testing and CI but typically not enabled in production builds.
ASan instruments every memory access, so it slows the program by about 2x and inflates memory usage. Use it for development and CI. Production builds usually compile without it.
-fsanitize=leak is a lighter-weight variant that only checks for leaks at exit, skipping the use-after-free and overflow checks. It's faster than ASan and useful for leak detection alone without the rest of the address-safety instrumentation. The output format is the same as ASan's leak section.
For most workflows, plain -fsanitize=address is the right default because it catches more bugs. Use -fsanitize=leak alone when ASan's runtime cost is too high for a specific test suite.
Valgrind is a standalone tool that runs an unmodified binary inside a virtual CPU. It catches the same family of bugs as ASan, including leaks, without requiring a recompile. The trade-off is speed; Valgrind runs the program 10x to 50x slower than native, depending on the workload.
A Valgrind leak report looks like this.
Valgrind classifies leaks into four categories. "Definitely lost" means no pointer anywhere in the program reaches the block. "Indirectly lost" means the block is referenced only by other lost blocks (a leaked tree, for example). "Possibly lost" means a pointer to an interior address still exists but the start of the block is unreachable. "Still reachable" means a pointer exists at exit, which is usually fine (global caches, singletons that never get torn down).
Valgrind is the older of the two main options. ASan is generally preferred for new projects because it's faster and integrates directly with the compiler. Valgrind is the right choice for inspecting a binary that wasn't built locally, or chasing a bug ASan doesn't catch.
On Windows with MSVC, the C runtime ships with a debug heap that tracks allocations and can dump unfreed blocks at exit. Enable it with _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF) near the start of main. Output goes to the debugger output window. It's coarser than ASan's reports but useful on a Windows-only build without ASan support. (Recent MSVC versions do support ASan, so this is mostly a legacy fallback.)
Static analyzers read source code without running it and flag suspicious patterns. They catch some leaks at compile time that runtime tools only catch when the buggy path actually executes.
cppcoreguidelines-* checks and it warns on raw-pointer ownership patterns that look risky.Static analyzers complement runtime tools rather than replace them. They produce false positives (warnings on safe code that looks risky) and false negatives (real bugs they don't see). Running them in CI alongside ASan-instrumented tests catches most of what either tool would catch alone.
| Tool | Catches leaks | Catches UAF | Speed cost | Setup |
|---|---|---|---|---|
| AddressSanitizer | Yes | Yes | ~2x slower | Recompile with -fsanitize=address |
| LeakSanitizer alone | Yes | No | ~1.1x slower | Recompile with -fsanitize=leak |
| Valgrind | Yes | Yes | 10-50x slower | Run unmodified binary |
| MSVC CRT debug heap | Yes | Partial | Small | Enable flag in code |
| clang-tidy / cppcheck | Some patterns | Some | Compile-time | Run against source |
A leak report is a stack trace plus a count. The trace shows where the leaked allocation was made, not where the leak "happened" (there's no single line where a leak happens; a leak is a missing line). Read from the top down.
Frame #0 is the allocator. Frame #1 is the C++ line that called new. The rest is the call stack at the moment of allocation. To fix the leak, look at every code path that runs _after_ that allocation. One of them is supposed to free it and doesn't.
"Direct" versus "indirect" matters for compound objects. A leaked struct that itself owns a heap allocation produces both: a direct leak for the outer struct and an indirect leak for whatever it pointed at. Fix the direct leak first; the indirect one usually goes away with it.
Two practical tips. Run with ASAN_OPTIONS=detect_leaks=1 if leak detection isn't on by default on a given platform. And a report with no symbols (just hex addresses) means it was compiled without -g; recompile with debug info and rerun.
10 quizzes