Skip to content
VLSI Mentor

C++ Part I · Module 10 · Dynamic Memory and Object Lifetime

Memory Leaks

This is the failure mode that does not announce itself. A leaked object produces no crash, no compiler warning, no sanitizer message in the default build and no wrong answer anywhere — the program computes exactly what it should and merely holds on to memory it has finished with. In a utility that runs for a second and exits, that is genuinely almost harmless, which is why the bug survives so comfortably in codebases and why so many engineers have never actually watched one do damage. The damage requires a program that keeps running, and the audience for this curriculum writes exactly those: reference models driven for millions of cycles, scoreboards accumulating transactions across a regression, long sweeps that run overnight. So this chapter measures the thing rather than asserting it, with one loop written twice and a footprint sampled as it goes, and the difference between the two versions is the difference between a flat line and a climb into the hundreds of megabytes. The second half is about finding leaks by method instead of by hopeful re-reading, including a tool that prints the source line responsible — and an honest account of why the sanitizer this curriculum has leaned on for every other memory bug cannot help with this one on the machine these measurements were taken on.

Intermediate17 min readC++Part IFundamentalsMemory

Part I · Module 10 · Chapter 10.6 · Memory Leaks

1. What a Leak Is

Chapter 10.5 gave the definition and the picture:

A leak is a lifetime that never ended.

And its trace showed what that looks like — five + lines, four - lines, one destructor that never ran. This chapter is the quantitative companion: how big a problem that is, and how to find them.

Note what a leak is not. It is not a crash, not a wrong answer, and not something the compiler will mention. A leaking program is correct in every observable way except for how much memory it holds.

2. The Shapes

Four ways the lifetime fails to end, in rough order of how often they occur:

The forgotten delete. The plain case, and the rarest in real code because it is the easiest to spot.

The early exit. A return, break or throw between the new and the delete. This is the common one, it is the one careful programmers write, and Chapter 10.8 is devoted to it.

The overwritten pointer. The last handle to an object is reassigned, and the object becomes unreachable and unreleasable:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    int* p = new int(1);
    p = new int(2);          // the first object is now unreachable: leaked
    delete p;                // releases the second only

The container of pointers. A vector<Widget*> destroyed releases the pointers and not the objects. Chapter 19.2 and Module 14 return to this; it is worth knowing early because it looks tidy and is not.

3. Why It Matters — Measured

A leaking loop of the shape a transaction-processing model actually has:

Azvya Education Pvt. Ltd.VLSI Mentor
leakloop.cpp — the relevant part
struct Transaction { int id; char payload[1024]; };

for (int i = 1; i <= 200000; ++i) {
    Transaction* t = new Transaction;
    t->id = i;
    // ... process it ...
    // and never release it
    if (i % 50000 == 0) std::printf("%10d %15ld\n", i, residentKB());
}

residentKB() asks the operating system how much memory this process currently has resident. Measured:

IterationsResident KB — leakingResident KB — with delete t;
01,3121,312
50,00068,2561,328
100,000135,1841,328
150,000168,8801,328
200,000195,8081,328

One line of difference between a flat line and 191 MB of growth.

The corrected version is perfectly flat — 1,328 KB from iteration 50,000 to iteration 200,000 — because each allocation reuses the block the previous one released. The leaking version has no such ceiling; 200,000 iterations is where I stopped, not where it stopped.

One honest qualification about that curve. The leaking column is not a straight line: 68 MB, 135 MB, 169 MB, 196 MB. The increments fall off. Resident footprint is the pages the OS currently has resident for the process, which tracks allocation but is not the same as it — the kernel's accounting and the allocator's own behaviour both affect it. The leak is real and unbounded; the exact shape of that column is partly an artefact of what is being sampled. For the count of leaked objects rather than resident pages, see §6, which gives an exactly linear answer.

4. Why a Long-Running Model Cannot Tolerate One

For a command-line tool that exits in a second, §3's bug is close to harmless. The operating system reclaims everything at exit, and 191 MB held briefly costs nothing.

Chapter 1.5 established what this audience actually builds, and none of it exits in a second.

A reference model driven for millions of cycles. At one leaked transaction per cycle, §3's rate is roughly a kilobyte per cycle. Ten million cycles is ten gigabytes. The model does not get a wrong answer; it gets killed.

A regression that runs overnight. The failure arrives after hours, in whichever test happened to be running, and that test gets blamed. The leak is then "intermittent" and "environment-dependent", which are the two words that make a bug expensive.

A simulator or EDA tool with plugins. A leak in a callback invoked per net, per cell or per timestep is multiplied by the design size — so it is invisible on a unit test and fatal on a real netlist.

And the thing nobody plans for: it gets slower first. Before it dies, a growing footprint degrades locality and eventually pushes the machine into swapping. Chapter 9.3 measured a 60× penalty for a traversal the memory hierarchy disliked; a process whose working set has grown past what the hardware can hold is paying that kind of tax everywhere. The symptom reported is "the overnight run is slow", which sounds like a performance problem and is a lifetime bug.

The general statement: a leak converts an upper bound you had into one you do not. A model with a flat footprint runs for as long as you ask; the same model with one leak has a maximum runtime, and nobody knows what it is.

And Chapter 10.5 §4's point applies with full force: if the destructor would have closed a file handle or released a simulator object, the limit you hit first may not be memory at all. File descriptor exhaustion from a leaked handle looks nothing like a memory problem and has the same cause.

5. The Tool — and Why It Is Not AddressSanitizer

Every other memory bug in this curriculum has been demonstrated with AddressSanitizer. For leaks, on this machine, that is not available, and it is worth being exact rather than giving advice I have not run.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ ASAN_OPTIONS=detect_leaks=1 ./leaky_asan
==71682==AddressSanitizer: detect_leaks is not supported on this platform.
exit=134

$ clang++ -std=c++17 -fsanitize=leak -g leaky.cpp -o leaky_lsan
clang++: error: unsupported option '-fsanitize=leak' for target
         'arm64-apple-darwin25.5.0'

LeakSanitizer is not supported on arm64 macOS, standalone or via ASan, and Valgrind is not installed and does not support this platform either. On Linux, -fsanitize=address with detect_leaks=1 is the standard answer and it works well — that is where most CI runs, and it is the right thing to configure there. It is simply not what produced the numbers below.

What is available here is macOS's own leaks:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ MallocStackLogging=1 leaks --atExit -- ./leaky_plain

On a program that leaks one int:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
Process 71778: 190 nodes malloced for 31 KB
Process 71778: 1 leak for 16 total leaked bytes.

STACK OF 1 INSTANCE OF 'ROOT LEAK: <malloc in main>':
3   leaky_plain      0x10276c4b8 main + 32  leaky.cpp:3
2   libc++abi.dylib  operator new(unsigned long) + 52
    1 (16 bytes) ROOT LEAK: <malloc in main 0xc34c2c000> [16]

Three things worth reading off it:

It names the source line — leaky.cpp:3, the new itself. That is what makes it a method rather than a hint.

16 bytes for a 4-byte int — Chapter 10.3 §7's measured slot size, confirmed by an independent tool.

MallocStackLogging=1 is what buys the stack trace. Without it you get the count and not the line.

6. The Negative Control

A tool's output means nothing until you have seen it on a correct program. The same source with delete p; added:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
Process 71795: 189 nodes malloced for 31 KB
Process 71795: 0 leaks for 0 total leaked bytes.

0 leaks, 0 bytes. And note the other number: 189 nodes versus 190. Those 189 allocations are the C++ runtime's own, present in both programs and not leaked. Without this control, the line "190 nodes malloced for 31 KB" looks like 31 KB of problem in a program that leaked 16 bytes.

Run the tool on the fixed program too. A detector you have only seen fail is not a detector you can trust.

This is the same discipline Chapter 10.4 §7 arrived at from the other direction, where ASan's silence on one mismatch direction was not evidence of correctness.

And on the loop from §3, at 20,000 iterations:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
Process 73417: 20189 nodes malloced for 25031 KB
Process 73417: 20000 leaks for 25600000 total leaked bytes.

Exactly 20,000 leaks — one per iteration — at 1,280 bytes each. That is the exactly-linear count §3's resident-footprint column could not give, and the 189 baseline nodes are visible again in the 20,189.

7. Finding Them Systematically

A method, in the order of cost:

Put a leak check in CI, on Linux, with ASan. This is the single highest-value step and it is cheap. A leak introduced today is trivial to find; the same leak in six months is archaeology.

Run the tool on the fixed build as well, per §6.

Watch the footprint of long runs. §3's table is a residentKB() call and a modulo. A model that reports its own footprint every N cycles turns an overnight mystery into a graph.

Make the test long enough to show the slope. A leak of a kilobyte per cycle is invisible over 100 cycles and obvious over 100,000. If the footprint is flat across two different run lengths, that is evidence; a single short run is not.

Count allocations against deallocations. Chapter 10.3 §3's instrumented operator new/operator delete reported allocations: 1 frees: 1. A mismatch at exit localises the problem without any external tool.

And read for the four shapes in §2 — but last, and knowing it is the weakest of these. Chapter 10.8's measurement is the reason: three leaks written by someone paying attention, with zero compiler warnings, in code that looks correct.

8. What to Actually Do

Configure leak detection in CI — ASan with detect_leaks=1 on Linux. Not supported on arm64 macOS; use leaks --atExit locally there.

Run the detector on a correct build too. 189 baseline nodes versus 190 is the difference between a signal and a scare.

Set MallocStackLogging=1 with leaks, or you get counts without source lines.

Have long-running programs report their own footprint periodically. It is a few lines and it converts a mystery into a slope.

Make leak tests long enough to show the slope, and compare two run lengths.

Count allocations against frees at exit for a cheap internal check.

Treat a flat footprint as a requirement for anything that runs for hours, not as a nicety.

Do not rely on reading code for this. It is the weakest method, and Chapter 10.8 measures why.

9. Mistakes Worth Making Once

Assuming a leak is harmless because the program exits. True for a one-second tool, false for everything in Chapter 1.5's list.

Testing for leaks on a short run. A kilobyte per cycle is invisible over 100 cycles.

Trusting a leak tool you have never seen report zero. The 190-vs-189 numbers are why.

Reading "190 nodes malloced" as the leak. 189 of them were the runtime's.

Forgetting MallocStackLogging=1. You lose the source line, which is the whole value.

Reaching for -fsanitize=leak on arm64 macOS. Measured: unsupported, for that exact target.

Overwriting the last pointer to an object. p = new int(2) after p = new int(1) leaks the first, silently.

Destroying a container of raw pointers and believing the objects went with it.

Concluding "no leaks" from a passing CI run whose tests never took the error path. The callout's subject.

10. Interview Relevance

"What is a memory leak and why does it matter?" A lifetime that never ended, so the destructor never ran. The answer that lands is a concrete consequence — "a model leaking a kilobyte per cycle over ten million cycles is ten gigabytes, so the run has an upper limit nobody measured" — rather than "you run out of memory".

"How would you find one?" ASan with detect_leaks=1 in CI, footprint logging in long runs, and allocation/deallocation counts. Mentioning that you would also run the detector on a passing build to establish the baseline is the mark of someone who has actually used one.

"Can a leak make a program slow?" Yes, and this is a good discriminator: a growing working set degrades locality and eventually swaps, so the reported symptom is often "slow overnight run".

"Are leaks caught by the compiler?" No. Zero warnings, in every case measured in this module.

"If the OS reclaims it all at exit, why care?" Because the programs that matter do not exit — and because the same missing destructor also failed to close files and release locks.

11. What You Should Be Able to Say Now

  1. A leak is a lifetime that never ended — no crash, no warning, no wrong answer.
  2. Four shapes: forgotten delete, early exit, overwritten pointer, container of raw pointers.
  3. Measured: 200,000 iterations leaking → 195,808 KB; the same loop with delete t; → flat 1,328 KB.
  4. The corrected version is flat because each allocation reuses the released block.
  5. The resident-footprint curve is not linear — resident pages track allocation without equalling it. The honest linear figure is the object count.
  6. Measured: exactly 20,000 leaks for 20,000 iterations, 1,280 bytes each.
  7. A leak converts a runtime bound you had into one you do not.
  8. It can present as slowness before it presents as exhaustion — Chapter 9.3's locality penalty.
  9. ASan leak detection is unsupported on arm64 macOS — measured, for both detect_leaks=1 and -fsanitize=leak. It is the right tool on Linux.
  10. leaks --atExit with MallocStackLogging=1 reports the leak and the source line.
  11. The negative control matters: 0 leaks on the fixed build, and 189 baseline nodes versus 190.
  12. Reading code is the weakest method, and the leaks that ship are unreachable deletes rather than missing ones.

12. Exercises

Exercise 1 — name the shape. Classify each by §2: (a) p = new int(2); after p = new int(1); (b) return false; between a new and its delete (c) a vector<Widget*> going out of scope (d) a new in a function with no delete anywhere.

Exercise 2 — do the arithmetic. At §3's measured rate, compute the footprint of a model leaking one Transaction per cycle over ten million cycles, and say what the practical consequence is.

Exercise 3 — why flat? Explain why the corrected loop stayed at exactly 1,328 KB across 150,000 further iterations.

Exercise 4 — read the tool. Given "190 nodes malloced for 31 KB" and "1 leak for 16 total leaked bytes", say what each number means and which one is the bug.

Exercise 5 — design the test. Write the plan for a leak test on a model that leaks a kilobyte per cycle, stating the run lengths and the comparison. Say why a single run is not evidence.

Exercise 6 — the platform. A colleague's CI leak check passes on macOS. Explain why that may be meaningless and what to change.

Exercise 7 — the hard case. Explain why a leak checker in CI can report zero on code that leaks in production, using the callout's reasoning.

13. Summary

  • A leak is a lifetime that never ended — silent, with no crash, warning or wrong answer.
  • Four shapes, of which the early exit is the one that reaches production (Chapter 10.8).
  • Measured: 195,808 KB leaking against flat 1,328 KB corrected, over 200,000 iterations.
  • The resident curve is not linear — the exactly-linear measure is the leak count: 20,000 leaks, 1,280 bytes each.
  • A leak removes a runtime bound and can present as slowness first.
  • ASan leak detection is unsupported on arm64 macOS (measured); it is correct on Linux.
  • leaks --atExit + MallocStackLogging=1 gives the count and the source line.
  • Always run the detector on a correct build — 189 baseline nodes against 190.
  • Code reading is the weakest method, and detection is necessary but not sufficient.

A leak is the failure that costs you nothing immediately, and that is what makes it tolerable long enough to become expensive. The next chapter is the opposite kind: two bugs that do their damage at the moment they happen, to an object whose lifetime has already ended. What makes them worth a chapter of their own is not that they are destructive but that they are unreliable about it — one of them, measured here, let the program print a plausible number and exit zero with no diagnostic of any kind, while its near-twin killed the process immediately. A bug that sometimes behaves itself is more dangerous than one that always crashes, and the two measured side by side show exactly why.

Standards & specifications

Governing standard
ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)

Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C++ Programming curriculum.