Lesson 4 of 7
A memory leak is heap memory you asked for and never gave back. The program still has it reserved, but has lost every pointer to it, so it can neither use it nor free it.
The reason leaks are hard is in the previous lesson's exercise: a leaking program looks completely normal. It prints the right answers and exits with status 0. Nothing tells you.
So the first job is making one visible.
A leak you can watch happen
C++ lets you replace the global allocation functions, which is how we can count every new and
delete the program performs:
#include <iostream>
#include <cstdlib>
#include <new>
int allocations = 0;
int deallocations = 0;
void* operator new(std::size_t size) {
++allocations;
void* p = std::malloc(size);
if (!p) {
throw std::bad_alloc();
}
return p;
}
void operator delete(void* p) noexcept {
if (p) {
++deallocations;
}
std::free(p);
}
void operator delete(void* p, std::size_t) noexcept {
if (p) {
++deallocations;
}
std::free(p);
}
void leaky() {
int* p = new int(1);
(void)p; // never deleted
}
void clean() {
int* p = new int(2);
delete p;
}
int main() {
clean();
clean();
leaky();
leaky();
leaky();
std::cout << "allocations: " << allocations << "\n";
std::cout << "deallocations: " << deallocations << "\n";
std::cout << "still held: " << (allocations - deallocations) << "\n";
return 0;
}
allocations: 5
deallocations: 2
still held: 3
Three allocations were never returned. The program printed its output and exited normally — the only evidence is the counter we added.
This is a teaching device, not a technique to use in real code. Module 11's AddressSanitizer does the same job properly, with the file and line of every leak.
One caveat if your numbers differ: the exact counts depend on your standard library. Some implementations allocate internally for streams or short strings, which shows up in the totals. What matters is the difference between allocations and deallocations, not the absolute figures.
Losing the pointer
The purest form of a leak is overwriting the only pointer you had:
#include <iostream>
int main() {
int* p = new int(1);
p = new int(2); // the first int is now unreachable — leaked
std::cout << *p << "\n";
delete p; // frees the second one only
return 0;
}
2
The first allocation is still reserved, and no pointer in the program refers to it. Nothing can free it now. That memory is lost until the process exits.
Leaks in a loop are the dangerous kind
A leak of 4 bytes once is harmless. The same leak in a loop that runs for hours is a program that gradually eats the machine:
#include <iostream>
#include <cstdlib>
#include <new>
int outstanding = 0;
void* operator new(std::size_t size) {
++outstanding;
void* p = std::malloc(size);
if (!p) {
throw std::bad_alloc();
}
return p;
}
void operator delete(void* p) noexcept {
if (p) {
--outstanding;
}
std::free(p);
}
void operator delete(void* p, std::size_t) noexcept {
if (p) {
--outstanding;
}
std::free(p);
}
void handle_request() {
int* buffer = new int[64];
buffer[0] = 1;
// forgot delete[]
}
int main() {
for (int i = 0; i < 1000; ++i) {
handle_request();
}
std::cout << "outstanding allocations after 1000 requests: " << outstanding << "\n";
return 0;
}
outstanding allocations after 1000 requests: 1000
A thousand requests, a thousand leaked blocks. A real server handles millions, and this is how a process that was fine on Monday is killed by the operating system on Thursday.
"The OS cleans up at exit, so who cares?"
It does — when the process exits, the operating system reclaims everything. So a leak in a program that runs for a second and quits genuinely does not matter much.
It matters when:
- The program is long-running. Servers, daemons, games, anything with a main loop.
- The leak is per-iteration. Small × many = large.
- The resource is not memory. File handles, sockets, and locks have much lower limits than memory, and running out of file descriptors fails in confusing ways.
- You are looking for other bugs. A leaky program makes tools noisy, so real problems hide.
And relying on process exit means the code is wrong but currently harmless — which lasts until someone calls it in a loop.
The fix is structural
#include <iostream>
#include <vector>
void handle_request() {
std::vector<int> buffer(64); // freed when the function returns
buffer[0] = 1;
}
int main() {
for (int i = 0; i < 1000; ++i) {
handle_request();
}
std::cout << "1000 requests, nothing leaked, no delete written\n";
return 0;
}
1000 requests, nothing leaked, no delete written
You cannot forget a delete you never had to write. That is the whole argument for RAII, made
concrete.
Try it
Take the counting harness from the first example and use it to prove that std::vector frees
its memory. Allocate a vector in a scope, print the outstanding count inside and after.
Checkpoint
- A leak is heap memory you can no longer reach and can no longer free.
- Leaking programs behave normally and exit cleanly — nothing reports the problem.
- Overwriting the last pointer to an allocation leaks it immediately.
- A small leak inside a loop is what kills long-running processes.
- Process exit reclaims memory, which makes leaks look harmless right up until they are not.
- Non-memory resources leak too, and run out far sooner.
- The fix is to stop writing the allocation, not to remember the deallocation.
Questions about this lesson
Log in to ask a question.