C++ Memory Leaks — 1.5 GB After 50 Tab Cycles
RSS grew to 1.5 GB after 50 tab cycles from circular refs.
20+ years shipping performance-critical C and C++ systems. Written from production experience, not tutorials.
- ✓Deep production experience
- ✓Understanding of internals and trade-offs
- ✓Experience debugging complex systems
- Memory leak: allocated heap memory that is never freed
- Detection tools: Valgrind (memcheck), AddressSanitizer (ASan)
- Prevention: RAII, smart pointers (unique_ptr, shared_ptr, weak_ptr)
- Performance impact: RSS growth ~1-10 MB/hour typical, OOM kill at 3-5 GB
- Production gotcha: leak detection tools add 2-10x slowdown, disable in production
- Biggest mistake: relying on manual new/delete instead of RAII wrappers
A memory leak occurs when a program allocates memory from the heap (using new, malloc, or calloc) and never deallocates it. The leaked memory remains allocated until the process exits. The operating system reclaims it on process termination, but during the process lifetime, the leak gradually reduces available heap space.
Leaks happen because C++ does not have automatic garbage collection. The programmer must explicitly pair every allocation with a deallocation. Real-world causes include:
- Exception thrown before delete is reached.
- Early return paths that skip cleanup.
- Overwriting the only pointer to allocated memory (pointer reassignment without delete).
- Circular references involving shared_ptr.
- Missing virtual destructors, causing derived resource not to be released.
Imagine you're renting storage units. Every time you need to store something, you rent a new unit. A memory leak is when you forget to return the key after you've emptied the unit — the unit stays rented, the bill keeps growing, and eventually you run out of storage units entirely. In C++, your program rents memory from the heap using 'new' and must return it using 'delete'. When it forgets to return that memory, it leaks — silently, invisibly — until your server crashes at 3am.
Memory leaks are the silent killers of C++ applications. Unlike a crash that screams at you immediately, a memory leak is a slow bleed — your process's RSS (Resident Set Size) climbs steadily over hours or days, swap starts thrashing, and eventually the OOM killer on Linux terminates your process with no warning. This is exactly what happened in a famous 2015 Firefox bug where tab-related allocations were never freed, causing hundreds of megabytes of leak per browsing session. The C++ runtime has no garbage collector watching your back. Every byte you allocate is your responsibility to free.
The core problem is the gap between WHEN you allocate memory and WHEN you need to free it. Throw exceptions, early returns, complex ownership semantics, or circular references into the mix, and that gap becomes a minefield. Raw pointers in C++ carry no metadata about who owns them, who should free them, or whether they're still valid. The language gives you a chainsaw — powerful, but it doesn't stop you from cutting your own hand off.
By the end of this article, you'll understand exactly how the heap allocator works under the hood, why leaks happen even in 'careful' code, how to use Valgrind and AddressSanitizer to pinpoint leaks down to the exact line, and how to architect your code with RAII and smart pointers so that leaks become structurally impossible. You'll also leave with the exact vocabulary and depth to ace memory management questions in senior C++ interviews.
What Is a Memory Leak and Why Does It Happen?
A memory leak occurs when a program allocates memory from the heap (using new, malloc, or calloc) and never deallocates it. The leaked memory remains allocated until the process exits. The operating system reclaims it on process termination, but during the process lifetime, the leak gradually reduces available heap space.
Leaks happen because C++ does not have automatic garbage collection. The programmer must explicitly pair every allocation with a deallocation. Real-world causes include:
- Exception thrown before
deleteis reached. - Early return paths that skip cleanup.
- Overwriting the only pointer to allocated memory (pointer reassignment without delete).
- Circular references involving
shared_ptr. - Missing virtual destructors, causing derived resource not to be released.
// TheCodeForge — C++ memory leak example #include <iostream> class Resource { public: Resource() { std::cout << "Resource acquired\n"; } ~Resource() { std::cout << "Resource released\n"; } }; void leak_function() { Resource* r = new Resource(); // allocated // some condition that causes early return if (rand() % 2 == 0) { // missing delete r; -> memory leak return; } delete r; return; } int main() { for (int i = 0; i < 10; ++i) { leak_function(); } return 0; }
- Every allocation grabs a contiguous block from the free list.
- Every deallocation returns the block to the free list (coalescing neighbors).
- A leak means the block stays in use but is no longer reachable — the free list shrinks permanently.
- Fragmentation makes the problem worse: small leaks fragment the heap, causing future allocations to fail even if total free memory is large.
new must be paired with a delete or ownership transferred to a smart pointer.Heap Allocator Internals: How Leaks Steal Memory
The heap allocator (glibc's malloc, tcmalloc, jemalloc) manages a pool of memory blocks. When you call new, it finds a free block via the free list, marks it as used, and returns the address. When you call delete, it returns the block to the free list. Coalescing merges adjacent free blocks into larger ones.
A leak does not just lose memory — it also fragments the heap. If many small blocks are leaked in the middle of the heap, the free list contains many small holes. A request for a larger contiguous block may fail even though total free memory is sufficient. This is called external fragmentation.
Production allocators also maintain thread caches to reduce lock contention. Leaked memory in thread caches is even harder to track because it may appear as "in use" but not actually reachable by any pointer. Tools like Valgrind intercept every malloc/free call to track reachable pointers.
// TheCodeForge — heap fragmentation simulation #include <iostream> #include <vector> int main() { // allocate large blocks, free every other one -> fragmentation std::vector<int*> blocks; for (int i = 0; i < 100; ++i) { blocks.push_back(new int[1024]); // 4KB each } // free even indices for (size_t i = 0; i < blocks.size(); i += 2) { delete[] blocks[i]; } // now heap has 50 holes of 4KB each, interspersed with used blocks // a new request for a 200KB contiguous block may fail! int* big = new int[50*1024]; // 200KB if (!big) std::cout << "Allocation failed due to fragmentation\n"; else std::cout << "Allocation succeeded (if not fragmented)\n"; delete[] big; for (size_t i = 1; i < blocks.size(); i += 2) { delete[] blocks[i]; } return 0; }
--enable-prof to track fragmentation.RAII and Smart Pointers: Making Leaks Impossible
RAII (Resource Acquisition Is Initialization) ties resource lifetime to object lifetime. When an object goes out of scope, its destructor automatically frees the resource. This makes leaks structurally impossible if the resource is owned by an RAII wrapper.
std::unique_ptr: exclusive ownership, no copy, move-only. Leak-free because the destructor callsdelete.std::shared_ptr: shared ownership via reference counting. Leak only if circular references exist.std::weak_ptr: non-owning observer that breaks reference cycles.
Rule: Never use raw new/delete outside of a RAII wrapper or low-level allocator. The only exceptions are when writing custom containers or interfacing with C libraries.
// TheCodeForge — RAII with unique_ptr #include <memory> #include <iostream> struct Widget { ~Widget() { std::cout << "Widget destroyed\n"; } void doWork() const { std::cout << "Work\n"; } }; void process() { auto w = std::make_unique<Widget>(); // RAII w->doWork(); // no delete needed! destructor called automatically } int main() { process(); // Output: "Work" then "Widget destroyed" // No leak even if exception thrown inside process() return 0; }
- Destructors are called for stack unwinding on exceptions, returns, and normal scope exit.
- No manual cleanup needed — reducing cognitive load.
- unique_ptr is zero-overhead over raw pointer in release builds.
- shared_ptr has reference counting overhead (atomic increments) but often acceptable.
- weak_ptr breaks cycles: use when two objects reference each other.
shared_ptr in a graph-like structure where nodes hold pointers to each other. This creates a reference cycle. Neither node's reference count drops to zero and both leak.weak_ptr for one direction of the cycle.Smart Pointer Comparison Table: When to Use Which
Choosing the right smart pointer is critical for leak-free C++. Here's a side-by-side comparison of ownership semantics, overhead, and use cases.
| Pointer Type | Ownership | Copy Semantics | Overhead | Circular Reference Risk | Typical Use Case |
|---|---|---|---|---|---|
std::unique_ptr | Exclusive | Move-only | None (zero overhead) | No | Owning a resource that has a single owner (e.g., a container element, a factory product) |
std::shared_ptr | Shared (reference counted) | Copy increments ref count | Atomic increment on copy, control block | Yes (if cycles) | Shared ownership in DAGs or caches where no single owner exists |
std::weak_ptr | Non-owning observer | Copy does not affect ref count | Minimal | Breaks cycles | Breaking circular references; caching; optional back-pointers |
Raw pointer (T*) | None (must manage externally) | Copy just copies address | Zero | Depends on external management | Only for non-owning, non-owning aliases in performance-critical code or when interfacing with C |
Key Rules: - Default to unique_ptr. It's zero-cost and prevents leaks by design. - Only use shared_ptr when ownership is truly shared (multiple entities need to keep the object alive independently). - Always use weak_ptr when you need a back-reference: think parent->child hierarchies. - Avoid raw new/delete entirely. If you must use a raw pointer for performance, document ownership clearly.
#include <memory> #include <vector> // Example: unique_ptr for exclusive ownership std::unique_ptr<Widget> createWidget() { return std::make_unique<Widget>(); } // Example: shared_ptr for shared ownership class Node { std::shared_ptr<Node> next; std::weak_ptr<Node> prev; // break cycle }; // Example: weak_ptr for cache class Cache { std::map<int, std::weak_ptr<ExpensiveObject>> cache; public: std::shared_ptr<ExpensiveObject> get(int key) { if (auto sp = cache[key].lock()) return sp; auto obj = std::make_shared<ExpensiveObject>(); cache[key] = obj; return obj; } };
Using Valgrind and AddressSanitizer to Find Leaks
Two dominant tools for finding memory leaks:
Valgrind (memcheck): Runs the program under a synthetic CPU. Intercepts all malloc/free/new/delete calls. Tracks reachability by scanning memory for pointer values. Reports: - definitely lost: pointer to block overwritten, no way to free. - indirectly lost: block is only referenced by other lost blocks. - possibly lost: pointer to interior of block exists, but start address might be lost. - still reachable: pointer still exists but block not freed before exit (technically not a leak, but often suggests design issue).
AddressSanitizer (ASan): Compile-time instrumentation. For every allocation, it records a shadow byte that tracks access permissions. At runtime, it detects heap-use-after-free, buffer overflow, and memory leaks (with detect_leaks=1). Much faster than Valgrind (2-4x slowdown vs 10-50x).
Both tools should be used during development and in stress testing. They are too slow for production. Use core dumps and heap profiling in production.
# Compile with debug symbols (ASan) g++ -g -O0 -fsanitize=address -fno-omit-frame-pointer leak_example.cpp -o leak_asan # Run with ASan leak detection ASAN_OPTIONS=detect_leaks=1:abort_on_error=1 ./leak_asan # Compile for Valgrind (no sanitizer) g++ -g -O0 leak_example.cpp -o leak_valgrind # Run under Valgrind valgrind --tool=memcheck --leak-check=full --show-leak-kinds=all ./leak_valgrind # Example output from ASan: # ================================================================= # ==12345==ERROR: LeakSanitizer: detected memory leaks # Direct leak of 40 byte(s) in 5 object(s) allocated from: # #0 0x7f8c1c2b1b40 in operator new(unsigned long) # #1 0x400a1f in leak_function() /tmp/leak_example.cpp:14 # #2 0x400a76 in main /tmp/leak_example.cpp:22 # SUMMARY: AddressSanitizer: 40 byte(s) leaked in 5 allocation(s).
Windows-Specific: Using CRT Debug Heap to Detect Leaks
On Windows, the C Runtime (CRT) provides a built-in debug heap that can detect memory leaks without external tools. It is enabled in debug builds by default and reports leaks to the Output window and via _.CrtDumpMemoryLeaks()
Key functions: - _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF) — enable leak detection at program exit. - _CrtDumpMemoryLeaks() — dump all unreleased allocations at any point. - _CrtSetBreakAlloc(long) — break into debugger when a specific allocation number occurs (useful for tracking down a leak after seeing its allocation number in the report). - _CrtMemCheckpoint / _CrtMemDifference — compare snapshots to detect memory growth between two points.
Limitations: - Only works in debug builds (can be enabled in release with _DEBUG defined, but with performance impact). - Does not track all memory (e.g., allocations by third-party libraries using different runtimes). - Reports are not as detailed as Valgrind's stack traces (only allocation number, size, and file/line if source info is available).
Setup steps: 1. Add #define _CRTDBG_MAP_ALLOC before including <crtdbg.h> to map new to with file/line info. 2. Include new()<crtdbg.h> and call _CrtSetDbgFlag early in . 3. Optionally call main()_ before exit. 4. For per-thread tracking, use CrtDumpMemoryLeaks()_CrtMemCheckpoint in each thread's main function.
// TheCodeForge — Windows CRT Debug Heap example #define _CRTDBG_MAP_ALLOC #include <crtdbg.h> #include <iostream> int main() { // Enable leak detection on program exit _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF); // Break on allocation #123 (replace with actual number from report) // _CrtSetBreakAlloc(123); int* leak = new int[100]; // intentional leak // Dump all leaks at this point (optional, already done on exit) // _CrtDumpMemoryLeaks(); return 0; } // Output: // Detected memory leaks! // Dumping objects -> // {123} normal block at 0x00AABBCC, 400 bytes long. // Data: < > CD CD CD CD ... // Object dump complete.
_DEBUG and CRT heap enabled, run it on a canary server, and monitor the Output window for leak reports. This is a low-cost way to catch leaks before they reach production._CrtSetDbgFlag, _CrtDumpMemoryLeaks, and _CrtSetBreakAlloc to catch leaks during development.Production Debugging Without Tools: Core Dumps and Heap Analysis
When a process is OOM-killed, you lose the process. But if you enable core dumps (ulimit -c unlimited), the OS saves the entire memory image to a file. You can analyze it post-mortem: - Use gdb to inspect pointers and see what blocks are reachable. - Use pstack to get thread stacks. - Use malloc_info(0, stderr) to dump glibc's internal allocation statistics.
Heap profiling tools like gperftools (Google Performance Tools) or jemalloc's built-in profiler can be enabled at runtime with minimal overhead (~5%). They generate heap snapshots that you can diff over time to find growing allocations.
In production, never run Valgrind or ASan. Instead, enable lightweight heap profiling continuously. Set a threshold: if RSS grows more than 10% in an hour, alert the pager with a heap snapshot attached.
# Enable core dumps echo '/tmp/core.%p' | sudo tee /proc/sys/kernel/core_pattern ulimit -c unlimited # Run with gperftools heap profiler env HEAPPROFILE=/tmp/myapp.hprof HEAP_PROFILE_ALLOCATION_INTERVAL=2097152 ./myapp # Signal to dump current heap profile kill -USR2 $(pgrep myapp) # Analyze diff between two snapshots pprof --text --base=/tmp/myapp.hprof.0001.heap /tmp/myapp.hprof.0002.heap # Example pprof output: # Total: 256.0 MB # 85.0 MB (33.2%): 0x7f8c1c2b1b40 /usr/lib/libstdc++.so.6.0.30 # 40.0 MB (15.6%): my_func /home/user/myapp.cpp:150
coredumpctl (systemd) to compress and manage cores. Alternatively, limit by ulimit -c 2097152 (2 GB max).Common Leak Patterns and How to Fix Them
- Missing virtual destructor: Base class pointer to derived object. If base destructor is not virtual, derived destructor never runs, leaking derived members.
- Circular shared_ptr references: A->B and B->A both hold shared_ptr. Neither reference count reaches zero. Fix: use weak_ptr in one direction.
- Exception in constructor: If constructor throws after
newbut before assigning to a smart pointer, the raw allocation is leaked. Fix: use RAII wrappers for all allocations in constructor initializer lists. - Unhandled allocation failure:
newmay throwstd::bad_alloc. If not caught, the code after it may never execute, potentially leaking prior allocations. Fix: always handle exceptions or use nothrow new. - Forgetting to delete in setter/reassignment:
ptr = new X;overwrites old pointer without delete. Fix: always delete before reassigning, or use smart pointers. - Thread-local storage leaks: If a thread exits without cleaning up its TLS blocks, those blocks remain allocated until process death. Fix: register cleanup functions with
pthread_key_createdestructor.
// TheCodeForge — missing virtual destructor leak #include <iostream> class Base { public: ~Base() { std::cout << "Base destructor\n"; } // not virtual! }; class Derived : public Base { int* data; public: Derived() : data(new int[100]) {} ~Derived() { delete[] data; std::cout << "Derived destructor\n"; } }; int main() { Base* b = new Derived(); delete b; // calls ~Base() only, not ~Derived()! -> leak of data array return 0; }
Common Leak Patterns — Quick Checklist
Use this checklist to quickly identify and fix the most frequent memory leak patterns in C++ code.
| Pattern | Symptom | Fix |
|---|---|---|
| Missing virtual destructor | Derived resources not freed when deleting via base pointer | Declare base destructor virtual; use override in derived |
Circular shared_ptr | Two objects hold shared_ptr to each other; neither is freed | Replace one direction with weak_ptr |
| Exception in constructor | new allocates before smart pointer assignment; leak if exception thrown | Use make_unique or make_shared; allocate inside constructor initializer lists with RAII |
| Reassignment without delete | Overwriting a pointer without freeing old memory | Always delete before reassignment, or use smart pointers that handle assignment |
| Thread-local storage leak | TLS blocks not freed on thread exit | Register destructor via pthread_key_create or use thread_local RAII objects |
| Missed deallocation in error path | Early return before delete (e.g., validation failure) | Use RAII or smart pointers; never rely on manual cleanup after returns |
| Container holding raw pointers | STL container of raw pointers not deleted before container destruction | Use vector<unique_ptr<T>> or vector<shared_ptr<T>> |
| Global/static scope memory | Memory allocated at module load time, never freed (still reachable) | Re-evaluate ownership; consider destructors via atexit or RAII singletons |
| Custom allocator mismatch | malloc/free mismatch with new/delete (e.g., using free on new'd array) | Use matching deallocation; prefer new/delete consistently |
Forgotten delete[] vs delete | Using scalar delete on array allocated with new[] | Use delete[] for array allocations; better yet, use vector or unique_ptr<T[]> |
Static Analysis and Coding Standards: Prevent Leaks at Compile Time
The best tool for memory leaks is prevention. C++ has evolved to make leaks harder to write: - -Wall -Wextra -Werror flags catch some cases. - clang-tidy with cppcoreguidelines-no-malloc, modernize-use-auto, modernize-use-equals-default. - cppcheck for static analysis: detects missing delete, mismatched allocation/deallocation, leaky containers. - -fsanitize=address as part of CI pipeline.
- Never use
new/deletedirectly (exception: custom allocators). - Use
std::make_uniqueandstd::make_shared. - Use RAII wrapper for every resource (file handle, socket, mutex).
- Prefer containers over raw arrays.
- Use C++17's
std::shared_ptrwithstd::make_sharedfor exception safety. - Mark classes with
finalif not intended for inheritance to avoid virtual destructor issues.
// TheCodeForge — clang-tidy will flag this #include <memory> class MyClass { public: MyClass() { ptr = new int[100]; // clang-tidy: warning: use make_unique } ~MyClass() { delete[] ptr; } private: int* ptr; }; // Better: class MyClassFixed { public: MyClassFixed() : ptr(std::make_unique<int[]>(100)) {} private: std::unique_ptr<int[]> ptr; };
clang-tidy and cppcheck to your code review CI. Reject PRs that introduce any new new or delete outside of low-level modules.cppcoreguidelines checks and banning raw new/delete in code reviews, the leak rate dropped to zero within one release cycle.Manual Leak Hunting: What the Tools Don't Tell You
Valgrind and ASan are your first line of defense, but they're not magic. When you're dealing with a 500k-line codebase that links against third-party shared objects you can't rebuild, static analysis and runtime tools will miss things. You need to know how to read a core dump's heap fragments and trace allocation chains by hand.
The first rule: never allocate memory at module load time. Static initialization order fiasco will hide leaks that only manifest during shutdown. Always allocate when first needed, not when the DLL loads. Second rule: tag your allocations. Use the new operator with a filename and line number macro. In production builds, I wrap every new call in a macro that stores the allocation site in a thread-local ring buffer. When the process OOMs, I dump that buffer to find the offender.
Third rule: audit your destructors. Virtual destructors in base classes aren't optional if you're deleting through a base pointer. Miss one, and you leak an entire derived object's memory silently. I've seen this crash production services after three days of uptime.
Heap Snapshots: Finding Leaks Without Tearing Down the Process
Sometimes you can't restart the application. Maybe it's a payment processing gateway with an SLA of 99.999%. Or a medical device firmware that boots for months. You need to diagnose memory bloat while the process keeps running. That's where heap snapshots come in.
On Linux, you can dump the heap mappings directly from /proc/<pid>/maps and examine all [heap] regions with gdb --pid <pid>. In gdb, use info malloc-stats if you're running with glibc's mtrace. For a raw approach, attach with gdb, run call to print summary stats to stderr, then inspect specific allocation sizes that grow over time.malloc_stats()
On Windows, you can call _ at intervals in a background thread and compare state dumps. The CRT debug heap stores a linked list of all live allocations — you can walk it with CrtMemCheckpoint()_ on a timer. Pair this with a metric like 'total heap bytes' exported to Prometheus and you'll catch the drift before it kills the node.CrtMemDumpAllObjectsSince()
Custom Allocator Hooks: When You Need Surgical Precision
Valgrind instruments every load and store — that's 5-20x slowdown. Unacceptable for latency-sensitive code. The solution is a lightweight custom allocator with built-in leak detection. I've shipped this pattern in trading systems where every microsecond counts.
The idea is simple: override operator new and operator delete at the translation unit level (don't touch global new unless you own every allocation). Your custom allocator maintains a concurrent hash map keyed by the allocation address. On allocation, store the call stack (use on Linux, backtrace()CaptureStackBackTrace() on Windows) and the allocation size. On deallocation, remove the entry. Once per minute, dump any entries older than N seconds — those are leaks.
The performance cost is one hash map insert per allocation (~50ns with a good hash), zero cost on deallocation (just a lookup and erase). That's 100x faster than AddressSanitizer. The tradeoff: you only catch allocations through your custom allocator, so malloc or mmap calls slip through. But for internal subsystems, this is production-grade leak detection that doesn't kill performance.
False Positives: The Tools Are Wrong, Now What?
Valgrind and AddressSanitizer are not omniscient. They report allocations that are reachable but never freed as leaks. That's correct for the heap, but it doesn't mean your code is broken. The most common false positive: a global singleton that lives for the entire process lifetime. The memory is freed by the OS on exit — there's zero practical leak. Yet the tool screams at you.
Why does this matter? Because rookie devs waste hours chasing ghosts. They add destructors to globals, introduce static order-of-initialization bugs, and generally make a mess. The senior move: classify each leak report. Green: actual leak (allocation grows over time). Yellow: reachable-on-exit (benign). Red: tool artifact (e.g., STL internal caches).
Suppress false positives explicitly. Valgrind has --suppressions, ASan has __attribute__((no_sanitize("address"))). Document every suppression with a reason. That turns a noisy report into a signal you can trust in CI.
// io.thecodeforge — c-cpp tutorial #include <iostream> #include <vector> // Global cache: allocated once, never freed — but not a leak. static std::vector<int>* global_cache = new std::vector<int>(); int main() { global_cache->push_back(42); std::cout << "Cache size: " << global_cache->size() << "\n"; // Valgrind will flag this as "still reachable" — suppress it. // Use: --suppressions=suppress_global.supp // Inside suppressions file: // { // <insert_a_suppression_name_here> // Memcheck:Leak // match-leak-kinds: reachable // fun:_zn6global_cache... return 0; }
Heap Snapshots: Find Leaks Without Killing Your Process
You can't restart production every time you suspect a leak. Heap snapshots let you compare the heap state at two points in time — without restarting. The core idea: take a baseline snapshot, run a suspect code path, take another snapshot, diff them. Any allocation that exists in the second snapshot but not the first is either a leak or a long-lived object.
On Linux, malloc_hook or LD_PRELOAD libraries can capture heap state. But the production-friendly method: use or mallinfo() to track arena size deltas over time. If arena size grows monotonically while RSS stays flat, you have internal fragmentation — not a leak.mallinfo2()
For surgical work, Google's GWP-ASan or heapprofd (Android) give you real-time allocation stacks. They sample high-frequency allocations and dump stack traces on every malloc. The trick: run them at 1% sample rate in production. The performance hit is ~2%. You get leak candidates without crashes. That's senior-engineer territory.
// io.thecodeforge — c-cpp tutorial #include <malloc.h> #include <iostream> void print_heap_stats(const char* label) { struct mallinfo mi = mallinfo(); std::cout << label << " — arena: " << mi.arena << " bytes, allocated: " << mi.uordblks << " bytes\n"; } void suspect_function() { for (int i = 0; i < 1000; ++i) { // Leak: allocate and never free int* p = new int(42); } } int main() { print_heap_stats("Before"); suspect_function(); print_heap_stats("After"); // Delta shows ~4000 bytes (1000 * sizeof(int)) return 0; }
mallinfo2() on glibc 2.33+. It returns size_t instead of int — no overflow on 64-bit heaps. Pair with a periodic timer in a debug build: log arena delta every 5 minutes. A steady upward slope is your leak fingerprint.The Firefox Tab Leak That Ate 400 MB Per Session
- Never assume GC handles circular references across C++/JavaScript boundaries.
- Always add explicit destructors or cleanup handlers for long-lived objects.
- Profile RSS growth over hours, not minutes — small leaks compound to megabytes.
- Use heap profiling tools early in development; retrofitting is expensive.
valgrind --tool=memcheck --leak-check=full --show-leak-kinds=all ./myappASAN_OPTIONS=detect_leaks=1:abort_on_error=1 ./myappcoredumpctl list | grep myapp; coredumpctl gdb myapp COREDUMPgdb -c core -ex 'info proc mappings' -ex 'thread apply all bt' -ex 'quit'valgrind --tool=helgrind ./myappASAN_OPTIONS=detect_race=1 ./myapp| Method | Overhead | Leak Types Caught | Production Suitability | Ease of Use |
|---|---|---|---|---|
| Valgrind Memcheck | 10-50x slowdown | All definite/possible loss | Not suitable (too slow) | Medium |
| AddressSanitizer | 2-4x slowdown | Use-after-free, leaks, overflows | Partial (canary instances) | Easy (compile-time flag) |
| Core dump analysis | 0% overhead (post-mortem) | Only if dump is complete | Yes (enable core dumps) | Hard |
| Heap profiling (gperftools) | ~5% overhead | Growing allocations (not necessarily leaks) | Yes (continuous) | Medium |
| RAII + Smart Pointers | 0% (design-time) | Prevents all ownership-related leaks | Always | Easy |
| Static Analysis (clang-tidy) | Compile-time only | Pattern-based leaks | Always | Easy |
| File | Command / Code | Purpose |
|---|---|---|
| leak_example.cpp | class Resource { | What Is a Memory Leak and Why Does It Happen? |
| fragmentation_sim.cpp | int main() { | Heap Allocator Internals |
| raii_example.cpp | struct Widget { | RAII and Smart Pointers |
| pointer_choice_example.cpp | std::unique_ptr | Smart Pointer Comparison Table |
| run_tools.sh | g++ -g -O0 -fsanitize=address -fno-omit-frame-pointer leak_example.cpp -o leak_a... | Using Valgrind and AddressSanitizer to Find Leaks |
| crt_debug_heap.cpp | int main() { | Windows-Specific |
| production_heap_profile.sh | echo '/tmp/core.%p' | sudo tee /proc/sys/kernel/core_pattern | Production Debugging Without Tools |
| virtual_destructor_leak.cpp | class Base { | Common Leak Patterns and How to Fix Them |
| clang_tidy_example.cpp | class MyClass { | Static Analysis and Coding Standards |
| SuppressFalsePositive.cpp | static std::vector | False Positives |
| HeapSnapshotDump.cpp | void print_heap_stats(const char* label) { | Heap Snapshots |
Key takeaways
Common mistakes to avoid
5 patternsForgetting to delete in early return paths
Missing virtual destructor in base class
override and = default in derived. Enable -Wsuggest-override compiler flag.Circular shared_ptr references
Reassigning a pointer without deleting the old value
Assuming memory is freed on thread exit without TLS cleanup
pthread_setspecific may leak if the thread exits without calling pthread_setspecific(key, NULL) first.pthread_key_create that frees the TLS block. Or use thread_local variables with RAII objects.Interview Questions on This Topic
Explain the difference between a memory leak and memory fragmentation. How would you diagnose each in a production system?
malloc_info(0, stderr) for glibc). Use mallctl for jemalloc. Look for many small free blocks. Also monitor RSS vs. virtual memory (VSZ) — large VSZ with moderate RSS often indicates fragmentation.
Fix: Leaks are fixed by adding missing deletes or using RAII. Fragmentation is mitigated by using memory pools, slab allocators, or jemalloc's background coalescing.Can std::shared_ptr still leak? If so, how and how do you prevent it?
shared_ptr can leak when circular references exist. For example, if object A holds a shared_ptr to B and B holds a shared_ptr to A, neither reference count ever drops to zero — the objects are never destroyed.
Prevention: Break the cycle by using weak_ptr in one direction. For example, in a parent-child tree, children store weak_ptr to parent (or a raw pointer with careful lifetime management).
Another scenario: forgetting to call reset() on a shared_ptr in a container (like a vector of shared_ptrs) when removing elements — the element may still be referenced elsewhere, but if not, the container holds the only reference and erase destroys it. That's not a leak, but a design issue.
Also, if you use make_shared, the control block and the managed object are allocated together in a single block — this prevents a leak if the constructor throws after allocation but before the shared_ptr is fully constructed.What is the role of weak_ptr in preventing memory leaks? Provide a real-world example.
weak_ptr breaks circular references by providing a non-owning, breakable reference. It does not contribute to the reference count. You must call lock() to obtain a shared_ptr from it; if the object has been destroyed, lock() returns empty.
Real-world example: A stock exchange order book where an Order object holds a shared_ptr to a Position (owning), and the Position may need to refer back to all its child orders. Using shared_ptr both ways would create a cycle. Instead, Position holds a vector<weak_ptr<Order>>. When an order is cancelled, the weak_ptr expires automatically. When the position is liquidated, it can iterate over its weak_ptrs, lock each one, and send cancellation requests only for still-valid orders.How would you debug a memory leak in a multi-threaded C++ application that only occurs under heavy load?
detect_leaks=1 and abort_on_error=1 on a stress test that mimics production concurrency. ASan will crash on the first leak, stalling the test. Use detect_leaks=1 only with exitcode=0 to let the test continue and collect all leaks.
3. Use Helgrind (Valgrind tool) to detect data races on shared pointer reference counts — that can corrupt counts and cause leaks.
4. Use jemalloc's heap profiling: `MALLOC_CONF=prof:trueWhat is a memory leak in C++? How does it differ from a buffer overflow?
Frequently Asked Questions
Yes — circular references with shared_ptr can still cause leaks. Also, if you use make_shared and the constructor throws after creating the memory block but before creating the shared_ptr, the block may leak (though this is rare with standard implementations). The safest approach is to use unique_ptr by default and only use shared_ptr where truly needed, with weak_ptr to break cycles.
definitely lost means the program overwrote the only pointer to a heap block — the block is permanently leaked and cannot be freed. still reachable means the program still has a pointer to the block at exit, but never freed it. This is often intentional (e.g., global singleton) but can indicate missing cleanup. Valgrind recommends treating still reachable as a leak if the pointer is in a scope that could exit before cleanup.
Yes — STL containers (vector, string, map, etc.) manage their own memory via allocators and destructors. When a container goes out of scope, its destructor frees all its allocated memory. However, if you store raw pointers in a container, you must delete each pointed-to object before destroying the container, or use a smart pointer container like vector<unique_ptr<T>>.
It is generally safe — it writes statistics to stderr without altering allocation state. But it holds an internal lock, so frequent calls can cause contention. Use it sparingly (e.g., once per minute under alert conditions). Also ensure stderr is not closed or redirected to a dangerous location.
Use heap profiling tools that work on live processes: gperftools' HeapProfilerStart() and HeapProfilerDump(), jemalloc's prof.active mallctl, or tcmalloc's MallocExtension::instance()->GetHeapSample(). These add about 5% overhead and can be toggled on/off programmatically or via signals (e.g., SIGUSR2 for jemalloc).
20+ years shipping performance-critical C and C++ systems. Written from production experience, not tutorials.
That's C++ Advanced. Mark it forged?
10 min read · try the examples if you haven't