C++ / MEMORY OWNERSHIP AND SMART POINTERS
new and delete and every way they leak
Locate the exact statement where a heap block's last pointer is lost and name which exit path skipped its delete.
What you will learn
- Trace every exit path from a new and prove exactly one delete runs on each
- Read a leak as a lost address rather than a forgotten free call
- Pair new with delete and new[] with delete[], and never cross the forms
- Prove a leak with a live-object counter or -fsanitize=address
Understanding new and delete and every way they leak
`new T(args)` does two things: it asks the allocator for a block big enough for a T, runs T's constructor in that block, and hands back the single address that records where the block lives. `delete p` undoes both, in the other order: it runs ~T on the object, then returns the block to the allocator. The pointer variable holding that address is an ordinary value with no destructor, so when it goes out of scope, gets overwritten, or is destroyed during stack unwinding, absolutely nothing happens to the block. A leak is therefore not a delete that failed; it is the moment the last copy of the address stops existing while the block is still allocated.
That turns leak hunting into a control-flow question. Take the region of code between a `new` and its `delete` and list every way control can leave it: `return`, `break`, `continue`, `goto`, and the one nobody draws on the diagram, a thrown exception. Almost any call in that region can throw, including an inner `new`, a `std::string` copy, a `push_back`, or `vector::at`, and unwinding jumps straight past your delete while destroying only the pointer. This is why the structural answer is to bind cleanup to a destructor instead of to a statement you have to reach, since destructors run on the throwing path too.
The remaining leak shapes are variations on the same two failures: losing the address, or never running the right destructor. Reassigning the sole owning pointer orphans the old object; putting raw pointers in a container means the container's destructor frees the pointer array and none of the pointees; copying an object whose members are owning raw pointers gives two deleters for one block, and the usual panic fix of deleting in neither leaks instead. Two pairings are hard rules, `new` with `delete` and `new[]` with `delete[]`, because the array form records an element count that `delete[]` needs to destroy every element, and crossing the forms is undefined rather than merely leaky. Leaks are also silent: no crash, no diagnostic, and the OS reclaims everything at exit, so a short test program hides them completely.
<iostream>
int live = 0;
struct Buf {
int id;
explicit Buf(int i) : id(i) { ++live; std::cout << "ctor " << id << "\n"; }
~Buf() { --live; std::cout << "dtor " << id << "\n"; }
};
void balanced() {
Buf* p = new Buf(1);
std::cout << "using " << p->id << "\n";
delete p; // the only path here reaches the delete
}
void overwritten() {
Buf* p = new Buf(2);
p = new Buf(3); // the address of Buf 2 is gone for good
delete p; // frees Buf 3 only
}
int scaled(int n) {
Buf* p = new Buf(n);
if (n < 0) return -1; // this exit never reaches the delete
int r = p->id * 10;
delete p;
return r;
}
int main() {
balanced();
overwritten();
int a = scaled(-1);
int b = scaled(7);
std::cout << "results " << a << " " << b << "\n";
std::cout << "still allocated: " << live << "\n";
}
A leak is losing the last pointer to a live heap block, so every path out of the allocating scope must reach exactly one matching delete.
Worked examples
A throw walks past the delete
Shows that unwinding destroys the pointer variable and leaves the object allocated, and that catching the exception cannot undo it.
<iostream>
<stdexcept>
int live = 0;
struct Node {
Node() { ++live; std::cout << "Node()\n"; }
~Node() { --live; std::cout << "~Node()\n"; }
};
void validate(int v) {
if (v > 10) throw std::runtime_error("value out of range");
}
void work(int v) {
Node* n = new Node;
validate(v); // a throw here skips everything below
std::cout << "worked on " << v << "\n";
delete n;
}
int main() {
try {
work(3);
work(42);
} catch (const std::exception& e) {
std::cout << "caught: " << e.what() << "\n";
}
std::cout << "live nodes: " << live << "\n";
}
Example explained
Line 1`Node* n = new Node;` stores the block's only address in a local pointer.
Line 2For v = 42 `validate` throws, and unwinding destroys `n` itself, a plain address value with nothing to clean up.
Line 3`delete n;` sits below the throw point, so it is never executed on that path.
Line 4`live nodes: 1` is the proof: one Node is still constructed and no code can name it any more.
A vector of raw pointers frees the pointers, not the objects
Demonstrates that clearing or destroying a container of raw pointers destroys only the pointer elements.
<iostream>
<vector>
int live = 0;
struct Item {
int v;
explicit Item(int x) : v(x) { ++live; }
~Item() { --live; }
};
int main() {
std::vector<Item*> a;
for (int i = 0; i < 3; ++i) a.push_back(new Item(i));
std::cout << "filled: " << live << "\n";
a.clear();
std::cout << "after clear: " << live << "\n";
std::vector<Item*> b;
for (int i = 0; i < 3; ++i) b.push_back(new Item(i));
for (Item* p : b) delete p;
b.clear();
std::cout << "after delete loop: " << live << "\n";
}
Example explained
Line 1`a.push_back(new Item(i))` copies an address into the vector; the vector has no idea it now owns anything.
Line 2`a.clear()` destroys three `Item*` elements, and destroying a pointer is a no-op, so live stays 3.
Line 3After the clear the three addresses existed nowhere else, which makes that leak permanent even inside the same function.
Line 4The explicit `for (Item* p : b) delete p;` is the only thing in the program that runs ~Item on those objects.
A constructor that throws leaks what it already allocated
Shows that a destructor never runs for an object whose constructor did not finish, so earlier member allocations are orphaned.
<iostream>
<stdexcept>
int live = 0;
struct Res {
char tag;
explicit Res(char t) : tag(t) { ++live; std::cout << "acquire " << tag << "\n"; }
~Res() { --live; std::cout << "release " << tag << "\n"; }
};
struct Pair {
Res* first;
Res* second;
explicit Pair(bool fail) : first(new Res('A')), second(nullptr) {
if (fail) throw std::runtime_error("stage two failed");
second = new Res('B');
}
~Pair() { delete first; delete second; }
};
int main() {
{
Pair ok(false);
}
std::cout << "live after ok: " << live << "\n";
try {
Pair bad(true);
} catch (const std::exception& e) {
std::cout << "caught: " << e.what() << "\n";
}
std::cout << "live after failure: " << live << "\n";
}
Example explained
Line 1In the successful case ~Pair runs both deletes, in member order, so A and B are released.
Line 2In the failing case `new Res('A')` has already succeeded when the constructor body throws.
Line 3~Pair is not called, because a destructor only runs for an object whose constructor completed; only fully built members are destroyed, and `Res*` has nothing to destroy.
Line 4`live after failure: 1` shows Res A is stranded, which is why cleanup belongs to each member rather than to the enclosing destructor.
Important notes
`delete p` does not modify `p`, and `delete nullptr` is a legal no-op, so `if (p) delete p;` buys nothing; setting the pointer to null afterwards guards against re-deleting it, not against leaking.
Deleting a derived object through a `Base*` whose destructor is not virtual is undefined behaviour; typically ~Derived never runs, so everything the derived part owned leaks even though the delete did execute.
Common mistakes
Putting the delete on the happy path and treating try/catch as cleanup: the throw unwinds past the delete, so every failure leaks one block and the catch has no way to recover the address.
Reassigning the owning pointer, as in `p = new Buf(i);` inside a loop, which orphans the previous object once per iteration, so the process grows steadily and only a long run makes it visible.
Calling `delete p` on memory that came from `new T[n]`: it is undefined behaviour, and in practice only the first element's destructor runs, so whatever the other elements owned leaks or the allocator aborts on a mismatched size.
Try it yourself
Change, predict, then run
Write `int countDigits(const char* s)` that begins with `int* tally = new int[10]{};`, returns -1 when `s` is null, and otherwise tallies digits and calls `delete[] tally`, bumping global `allocs` and `frees` counters at each new and delete. Print both counters from main after calling it with `nullptr` and with "a1b2", then make the counters match without moving the allocation out of the function.
Open the C++ workspaceCheck your understanding
You write `Buf* p = nullptr; for (int i = 0; i < 5; ++i) { p = new Buf(i); use(p); } delete p;`. When the delete has run, how many Buf objects are still allocated?
- 0, because the single delete releases every block the pointer ever held
- 1, because only the object from the last iteration is still allocated
- 4, because each reassignment discards the only address of the previous object
- 5, because delete on a pointer that was reassigned is undefined and frees nothing
Show answer
Each `p = new Buf(i)` overwrites the only stored address, so the objects from iterations 0 through 3 stay allocated with nothing pointing at them; the final delete frees exactly one block, the one from iteration 4. Option 1 is the mirror image of the truth: it counts what was freed instead of what leaked, and delete releases a single block, not the pointer's history.