Lesson 2 of 7
You are meeting this before new and delete on purpose. Most C++ courses teach manual memory
first and RAII as an advanced topic, which produces programmers who write leaky code and then
learn to patch it. The order here is deliberate: learn the thing that works, then see what it
protects you from.
The guarantee
A stack object's destructor runs when its block ends. Always. That is the entire foundation.
#include <iostream>
struct Noisy {
int id;
Noisy(int i) : id(i) {
std::cout << " built " << id << "\n";
}
~Noisy() {
std::cout << " destroyed " << id << "\n";
}
};
int main() {
std::cout << "entering block\n";
{
Noisy a(1);
Noisy b(2);
std::cout << " inside\n";
}
std::cout << "left block\n";
return 0;
}
entering block
built 1
built 2
inside
destroyed 2
destroyed 1
left block
Two things to notice.
Destruction is automatic. Nobody called ~Noisy. Leaving the block did it.
It runs in reverse order. b was built second and destroyed first. Objects are torn down in
the opposite order to construction, which matters when one depends on another.
Module 9 covers struct, constructors, and destructors properly. For now you only need the
guarantee.
Why "always" is the strong word
The destructor runs on every exit path, not just the one you were thinking about:
#include <iostream>
struct Guard {
~Guard() { std::cout << " cleaned up\n"; }
};
void early_return(bool bail) {
Guard g;
if (bail) {
std::cout << " returning early\n";
return; // destructor still runs
}
std::cout << " running to the end\n";
}
int main() {
std::cout << "call with bail=true:\n";
early_return(true);
std::cout << "call with bail=false:\n";
early_return(false);
return 0;
}
call with bail=true:
returning early
cleaned up
call with bail=false:
running to the end
cleaned up
Early return, a break, falling off the end, and — importantly — an exception being thrown
(Module 15) all run the destructor. There is no path out of that block that skips it.
That is what makes the guarantee usable. A cleanup you have to remember to write is a cleanup you
will eventually forget on the fifth return you add six months later.
RAII
Resource Acquisition Is Initialisation. Bad name, simple idea:
Wrap a resource in an object. Acquire it in the constructor, release it in the destructor. Then the resource cannot outlive the object, and the object cannot escape its scope.
The name emphasises acquisition; the destructor is the part that actually matters.
"Resource" means anything you must give back: heap memory, an open file, a network socket, a lock, a database connection.
#include <iostream>
// A toy resource that must be released. In real code this would be a file
// handle or a socket.
struct Connection {
int id;
bool open;
};
Connection open_connection(int id) {
std::cout << " opened " << id << "\n";
return Connection{id, true};
}
void close_connection(Connection& c) {
if (c.open) {
std::cout << " closed " << c.id << "\n";
c.open = false;
}
}
// The RAII wrapper: acquire in the constructor, release in the destructor.
struct ConnectionGuard {
Connection conn;
ConnectionGuard(int id) : conn(open_connection(id)) {}
~ConnectionGuard() { close_connection(conn); }
};
void risky_work(bool fail_early) {
ConnectionGuard guard(7);
if (fail_early) {
std::cout << " something went wrong\n";
return; // still closed
}
std::cout << " work finished\n";
}
int main() {
risky_work(false);
risky_work(true);
return 0;
}
opened 7
work finished
closed 7
opened 7
something went wrong
closed 7
The connection closes on both paths, and risky_work contains no cleanup code at all. Compare
with the manual version, where every return needs its own close_connection(c); and the one
you forget is a leaked connection in production.
You have been using RAII already
#include <iostream>
#include <string>
#include <vector>
int main() {
{
std::vector<int> v(1000); // allocates heap memory
std::string s(500, 'x'); // allocates heap memory
std::cout << v.size() << " " << s.size() << "\n";
} // both freed here, automatically
std::cout << "no leak, and no delete written anywhere\n";
return 0;
}
1000 500
no leak, and no delete written anywhere
std::vector and std::string are RAII types. Their constructors allocate, their destructors
free. Every container in the standard library works this way, as do std::unique_ptr and
std::shared_ptr (Module 10) and std::lock_guard (Module 21).
This is why the answer to "how do I avoid memory leaks in C++" is usually use the standard
library and stop writing new, not "be more careful."
What RAII does not do
It does not help with objects you allocate on the heap and never destroy. If you write
new ConnectionGuard(7) and never delete it, the destructor never runs. RAII only gives you
its guarantee for objects with automatic storage duration — locals in a scope.
That is the loophole new opens, and the subject of the next lesson.
Try it
Write a Timer struct that prints "start" in its constructor and "stop" in its destructor. Use
it in a function with several return paths and confirm "stop" prints on every one. Then nest
two timers and note the order.
Checkpoint
- A stack object's destructor runs when its block ends, on every exit path.
- Destruction happens in reverse order of construction.
- RAII: acquire a resource in a constructor, release it in a destructor.
- The resource then cannot outlive its owner, and cleanup cannot be forgotten.
std::vector,std::string, and the smart pointers are all RAII types.- RAII protects objects with automatic storage; heap objects you never destroy are exempt.
Questions about this lesson
Log in to ask a question.