Understanding Dynamic memory leaks
Dynamic Memory Allocation in C++
In C++, when you need to allocate memory dynamically (at runtime), you use the new operator to get memory from the heap. Unlike stack memory, heap memory is not automatically managed - you must manually free it using the delete operator.
Stack Memory Heap Memory
┌──────────────┐ ┌──────────────────┐
│ Automatic │ │ Manual Management│
│ Cleaned up │ │ YOU must call │
│ automatically│ │ delete! │
└──────────────┘ └──────────────────┘
↑ ↑
int x = 5; int* p = new int(5);
(destroyed when (YOU must delete p)
out of scope)
C++ does not have automatic garbage collection.
If you allocate memory with new, you must free it with delete.
If you forget, that memory is permanently lost until your program terminates - this is called a memory leak.
Let’s start with an example where the prgram calls new to allocate memory and delete to free the memory:
#include <iostream>
void good_function(int data) {
int* rawPtr = new int(data); // 1. Allocate memory from heap
std::cout << "data: " << *rawPtr << std::endl;
delete rawPtr; // 2. Free the memory
}
int main() {
good_function(10);
return 0;
}
Compile and run with Valgrind:
g++ -g -O0 good_example.cpp -o good_example
valgrind --leak-check=full ./good_example
Valgrind Report (No Leaks):
==3102717== HEAP SUMMARY:
==3102717== in use at exit: 0 bytes in 0 blocks
==3102717== total heap usage: 3 allocs, 3 frees, 73,732 bytes allocated
==3102717==
==3102717== All heap blocks were freed -- no leaks are possible
==3102717==
==3102717== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
Memory Lifecycle:
Step 1: int* rawPtr = new int(data)
STACK HEAP
┌──────────────┐ ┌──────────────┐
│ Function │ │ │
│ Stack Frame │ │ [4 bytes] │
│ │ │ value: 10 │
│ rawPtr ─────┼──────────────┼─> [int] │
│ (address) │ │ │
└──────────────┘ └──────────────┘
↑ ↑
Lives here Lives here until
(automatic) delete is called
Step 2: Using *rawPtr
STACK HEAP
┌──────────────┐ ┌──────────────┐
│ │ │ │
│ rawPtr ─────┼──────────────┼─> [10] │
│ (pointer) │ │ │
└──────────────┘ └──────────────┘
Access via pointer
Step 3: delete rawPtr ✅
STACK HEAP
┌──────────────┐ ┌──────────────┐
│ │ │ │
│ rawPtr │ X │ [freed] │
│ (dangling) │ │ │
└──────────────┘ └──────────────┘
Memory returned to OS
Step 4: Function exits
STACK HEAP
┌──────────────┐ ┌──────────────┐
│ │ │ │
│ [destroyed] │ │ [free] │
│ │ │ │
└──────────────┘ └──────────────┘
Stack cleaned up No leak! ✅
Now let’s see what happens when you forget to call delete:
#include <iostream>
void bad_function(int data) {
int* rawPtr = new int(data); // Allocate memory from heap
std::cout << "data: " << *rawPtr << std::endl;
// Forgot to delete!
}
int main() {
bad_function(10);
return 0;
}
Compile and run with Valgrind:
g++ -g -O0 forgot_delete.cpp -o forgot_delete
valgrind --leak-check=full ./forgot_delete
Valgrind Report - Observe the Memory Leak:
==3102369== HEAP SUMMARY:
==3102369== in use at exit: 4 bytes in 1 blocks
==3102369== total heap usage: 3 allocs, 2 frees, 73,732 bytes allocated
==3102369==
==3102369== Searching for pointers to 1 not-freed blocks
==3102369== Checked 147,280 bytes
==3102369==
==3102369== 4 bytes in 1 blocks are definitely lost in loss record 1 of 1
==3102369== at 0x4849013: operator new(unsigned long) (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==3102369== by 0x109201: bad_function(int) (memory_leak.cpp:5)
==3102369== by 0x10925D: main (memory_leak.cpp:10)
==3102369==
==3102369== LEAK SUMMARY:
==3102369== definitely lost: 4 bytes in 1 blocks
==3102369== indirectly lost: 0 bytes in 0 blocks
==3102369== possibly lost: 0 bytes in 0 blocks
==3102369== still reachable: 0 bytes in 0 blocks
==3102369== suppressed: 0 bytes in 0 blocks
==3102369==
==3102369== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
Memory Lifecycle:
Step 1: int* rawPtr = new int(data)
STACK HEAP
┌──────────────┐ ┌──────────────┐
│ Function │ │ │
│ Stack Frame │ │ [4 bytes] │
│ │ │ value: 10 │
│ rawPtr ─────┼──────────────┼─> [int] │
│ (address) │ │ │
└──────────────┘ └──────────────┘
↑ ↑
Lives here Lives here until
(automatic) delete is called
Step 2: Using *rawPtr
STACK HEAP
┌──────────────┐ ┌──────────────┐
│ │ │ │
│ rawPtr ─────┼──────────────┼─> [10] │
│ (pointer) │ │ │
└──────────────┘ └──────────────┘
Access via pointer
Step 3: Function exits (NO delete called!) ❌
STACK HEAP
┌──────────────┐ ┌──────────────┐
│ │ │ │
│ [destroyed] │ X │ [LEAKED!] │
│ │ │ value: 10 │
└──────────────┘ └──────────────┘
rawPtr is gone! Memory orphaned!
(pointer destroyed) No way to free it!
Step 4: Program continues
STACK HEAP
┌──────────────┐ ┌──────────────┐
│ │ │ │
│ │ │ [LEAKED] │
│ │ │ 4 bytes │
└──────────────┘ └──────────────┘
Permanently lost! ❌
The Problem: When bad_function() exits:
- The local variable
rawPtris destroyed (it’s on the stack) - But the memory it pointed to (on the heap) is not freed
- There’s now no way to access or free that memory
- The 4 bytes are permanently lost until the program terminates
Why This Matters at Scale:
int main() {
for (int i = 0; i < 1000000; i++) {
bad_function(i); // Leaks 4 bytes EVERY call!
}
// Total leaked: 4 MB of memory!
return 0;
}
In a long-running application:
- Memory consumption grows continuously
- System performance degrades
- Eventually: out-of-memory crashes
Even if you remember to call delete, exceptions can still cause leaks:
#include <iostream>
#include <stdexcept>
void some_function() {
throw std::runtime_error("Something went wrong");
}
void bad_function(int data) {
int* rawPtr = new int(data); // Allocate memory
std::cout << "data: " << *rawPtr << std::endl;
some_function(); // Exception thrown here!
delete rawPtr; // This line NEVER executes! ❌
}
int main() {
try {
bad_function(10);
} catch (const std::exception& e) {
std::cerr << "Caught: " << e.what() << '\n';
}
return 0;
}
Compile and run with Valgrind:
g++ -g -O0 exception_leak.cpp -o exception_leak
valgrind --leak-check=full ./exception_leak
Valgrind Report - Observe the Exception Leak:
data: 10
Caught: Something went wrong
==3106542==
==3106542== HEAP SUMMARY:
==3106542== in use at exit: 4 bytes in 1 blocks
==3106542== total heap usage: 4 allocs, 3 frees, 73,804 bytes allocated
==3106542==
==3106542== 4 bytes in 1 blocks are definitely lost in loss record 1 of 1
==3106542== at 0x4849013: operator new(unsigned long) (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==3106542== by 0x109381: bad_function(int) (memory_leak_exc.cpp:9)
==3106542== by 0x1093FD: main (memory_leak_exc.cpp:17)
==3106542==
==3106542== LEAK SUMMARY:
==3106542== definitely lost: 4 bytes in 1 blocks
==3106542== indirectly lost: 0 bytes in 0 blocks
==3106542== possibly lost: 0 bytes in 0 blocks
==3106542== still reachable: 0 bytes in 0 blocks
==3106542== suppressed: 0 bytes in 0 blocks
==3106542==
==3106542== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
The Surprising Result:
Observation: Even though we have delete rawPtr in the code, the memory still leaked!
- “4 bytes in 1 blocks are definitely lost” - Memory leaked despite
deletebeing present - The exception caused the function to exit before reaching
delete - Leak originated at line 9 (the
newstatement)
Exception Execution Flow:
Step 1: int* rawPtr = new int(data)
STACK HEAP
┌──────────────┐ ┌──────────────┐
│ Function │ │ │
│ Stack Frame │ │ [4 bytes] │
│ │ │ value: 10 │
│ rawPtr ─────┼──────────────┼─> [int] │
│ │ │ │
└──────────────┘ └──────────────┘
Step 2: some_function() throws exception ⚡
STACK HEAP
┌──────────────┐ ┌──────────────┐
│ │ │ │
│ rawPtr ─────┼──────────────┼─> [10] │
│ │ │ │
└──────────────┘ └──────────────┘
⚡ Exception! Still allocated!
Step 3: Stack unwinding begins
(cleaning up local variables)
STACK HEAP
┌──────────────┐ ┌──────────────┐
│ │ │ │
│ rawPtr │ ? │ [10] │
│ (being │ │ │
│ destroyed) │ │ │
└──────────────┘ └──────────────┘
Pointer about to die Memory still there!
delete rawPtr NEVER runs!
Step 4: Function exited via exception
STACK HEAP
┌──────────────┐ ┌──────────────┐
│ │ │ │
│ [destroyed] │ X │ [LEAKED!] │
│ │ │ value: 10 │
└──────────────┘ └──────────────┘
rawPtr destroyed! Memory orphaned!
delete line skipped! No way to free it! ❌
What happened:
- Memory is allocated with
new - Exception is thrown in
some_function() - Stack unwinding begins (C++ cleans up local objects)
rawPtr(the pointer variable) is destroyed during unwinding- The
delete rawPtrline is never reached - The allocated memory has no way to be freed
The Problems with Manual Memory Management
Managing heap memory manually with new/delete is error-prone because:
Problem 1: Easy to Forget
void process() {
int* data = new int[1000];
// ... lots of code ...
// Did you remember to delete[]? ❌
}
Issue: In complex functions with multiple return paths, it’s easy to forget delete.
Problem 2: Exception Safety is Hard
void process() {
int* data = new int[1000];
// Any function here might throw! ⚡
complexOperation(); // Exception?
delete[] data; // Might never execute!
}
Issue: Any exception between new and delete causes a leak.
Problem 3: Multiple Exit Points
void process(bool condition) {
int* data = new int[1000];
if (condition) {
return; // Oops, forgot delete! ❌
}
// ... more code ...
if (errorOccurred) {
return; // Oops again! ❌
}
delete[] data; // Only this path is safe
}
Issue: Each return path needs its own delete.
Problem 4: Ownership is Unclear
int* createData(); // Who deletes this?
void process(int* data); // Does this take ownership?
int* getData(); // Should caller delete?
Issue: Unclear who is responsible for freeing memory.
Detection Tools you can use to findout memory leaks
- Valgrind - Detects memory leaks at runtime
- AddressSanitizer - Fast leak detection during testing
- Static analyzers - Clang-Tidy, Cppcheck find potential leaks
Uhh So many problems, is there anyway these problems can be fixed. Yes the answer to all these problems is Smart pointers introduced in C++11 based on the RAII concept.