Detecting and preventing memory leaks in C++ is a two-part job: a tool tells you that memory was lost, and a set of habits stops it happening. The address sanitizer finds most leaks with no extra tooling at all, and the habits are mostly about ownership — making it obvious who is responsible for each allocation so that a leak becomes impossible rather than merely unlikely.
What a leak actually is
A leak happens when memory is still allocated at the moment the program ends. The program does not crash. It just quietly uses more and more memory, and on a long-running server that eventually brings the machine down.
#include
int* allocate_and_forget() {
int* block = new int[1000];
block[0] = 1;
return block; // the pointer is lost here
}
int main() {
for (int i = 0; i < 100; ++i) {
allocate_and_forget();
}
std::cout << "program finished normally\n";
return 0;
}The program prints one cheerful line and exits with status zero. A hundred thousand integers are gone, and nothing in the output says so.
The address sanitizer finds leaks for you
Adding -fsanitize=address to the compile and link step turns on a runtime checker that reports leaks at exit.
g++ -std=c++17 -Wall -Wextra -g -fsanitize=address leak.cpp -o leak
./leakThe report names the allocation site, not the place where the memory was lost, which is usually what you need:
=================================================================
==12345==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 4000 byte(s) in 1 object(s) allocated from:
#0 0x7f... in operator new(unsigned long)
#1 0x55... in allocate_and_forget() leak.cpp:5
#2 0x55... in main() leak.cpp:12
SUMMARY: AddressSanitizer: 4000 byte(s) leaked in 100 allocation(s).
=================================================================The three lines of the trace are the whole story: the allocation happened in allocate_and_forget on line 5, called from main on line 12. The fix is at the top of that trace, not at the bottom.
The leak checker in the standard library
The C++ standard library ships its own leak detector for the containers it owns. When a container is destroyed while it still holds elements, the library prints a warning.
#include
#include
struct Widget {
explicit Widget(int id) : id(id) {}
~Widget() {
std::cout << "destroying widget " << id << '\n';
}
int id;
};
int main() {
auto leaked = std::make_unique(10);
leaked[0] = 1;
std::cout << "allocated\n";
return 0;
}The output is clean, because the unique pointer released the array:
allocatedThe library's own check is triggered when a container is allocated with new and never deleted, which is why the advice is always the same: let a smart pointer own it.
Valgrind for the harder cases
Valgrind runs your program under instrumentation and reports leaks, invalid reads, invalid writes and uninitialised value use. It is slower than the sanitizer but it needs no recompilation.
valgrind --leak-check=full --show-leak-kinds=all ./programThe output separates the leaks into categories: *definitely lost*, which nothing points at any more, *indirectly lost*, which is reachable only through other leaked blocks, and *still reachable*, which is probably a global pointer that is never freed.
The *still reachable* category is where most people's first reaction is "that is a leak too". It usually is not: a pointer stored in a global that lives for the whole program is reachable, and whether that is a problem depends on your design.
Reading a leak report
A leak report is a stack trace of the allocation. Reading it means finding the frame in your own code, and the frames above it are usually library internals you can ignore.
The categories of leak are worth knowing.
- Directly lost — the block was allocated and nothing points at it any more.
- Indirectly lost — the block is only reachable through another leaked block.
- Still reachable — something still points at it, but the pointer is probably a global that will never be freed.
- Possibly lost — an interior pointer to the block exists, so the tool cannot prove it is unreachable.
Habits that prevent leaks
The reliable way to avoid leaks is to remove the decisions.
Prefer automatic storage. A local variable is freed at the end of its scope without any thought at all.
Use a smart pointer for every dynamic allocation. A std::unique_ptr makes the leak impossible, because the compiler will not let the ownership be ambiguous.
Do not return raw pointers. A function that returns a raw pointer has to document who deletes the result, and that documentation is the first thing to go stale.
#include
#include
struct Widget {
explicit Widget(int id) : id(id) {}
int id;
};
std::unique_ptr make_widget(int id) {
return std::make_unique(id);
}
int main() {
auto widget = make_widget(7);
std::cout << "widget id: " << widget->id << '\n';
return 0;
}The output shows the widget being created and cleaned up automatically:
widget id: 7Do not delete something you did not allocate. A double free is as bad as a leak, and the sanitizer reports both.
Keep ownership obvious in the signature. A function taking std::unique_ptr<T> takes ownership; a function taking T& or const T& borrows. The parameter type is the documentation.
A leak checklist
- Build your tests with
-fsanitize=addressfrom the start. - Read the first leak report completely before fixing anything.
- Fix leaks at the allocation site, not at the exit point.
- Treat *definitely lost* and *indirectly lost* as bugs.
- Investigate *still reachable* before deciding it is harmless.
- Never suppress a leak report without understanding it.
- Make the ownership visible in every function signature.
Quick reference
- A leak is memory still allocated when the program ends; the program does not crash.
-fsanitize=addressreports leaks at exit and names the allocation site.- Valgrind finds leaks without recompiling and also catches invalid memory access.
- Leaks are *definitely lost*, *indirectly lost*, *still reachable* or *possibly lost*.
- Automatic storage and smart pointers remove the possibility of a leak.
- Ownership belongs in the function signature:
std::unique_ptrowns, a reference borrows.
Try it yourself: write a program that allocates memory in a loop and loses the pointers, then build it with [[-fsanitize=address]] and read the report. Fix the leak using a container or a smart pointer and rebuild. Finally, introduce a double free instead and confirm the sanitizer reports that too, with a different message.