Lesson 4 of 12
A destructor runs when an object is destroyed. It is the constructor's counterpart, and it is where a class gives back whatever it took.
#include <iostream>
class Resource {
int id;
public:
Resource(int i) : id(i) {
std::cout << "acquire " << id << "\n";
}
~Resource() {
std::cout << "release " << id << "\n";
}
};
int main() {
std::cout << "start\n";
{
Resource a(1);
Resource b(2);
}
std::cout << "end\n";
return 0;
}
start
acquire 1
acquire 2
release 2
release 1
end
The syntax is ~ClassName(), no parameters, no return type, and there can be only one. Unlike
constructors, a destructor cannot be overloaded — there is only one way to die.
Lesson 8.2 showed this guarantee from the outside. Now you are writing the other half.
When exactly it runs
- Stack objects: at the closing brace of their scope, in reverse order of construction.
- Members: after the enclosing object's destructor body, in reverse declaration order.
- Heap objects: when you
deletethem — and never, if you forget. - Temporaries: at the end of the full expression that created them.
That third point is the loophole from lesson 8.2, and the reason smart pointers exist.
The class this module builds on
Here is a type that genuinely owns a resource — a hand-rolled dynamic array. We will keep returning to it, because every rule in the rest of this module is a rule about types like this one.
#include <iostream>
class Buffer {
int* data;
std::size_t count;
public:
Buffer(std::size_t n) : data(new int[n]), count(n) {
for (std::size_t i = 0; i < count; ++i) {
data[i] = 0;
}
std::cout << "Buffer(" << count << ") allocated\n";
}
~Buffer() {
delete[] data;
std::cout << "Buffer(" << count << ") freed\n";
}
void set(std::size_t i, int v) { data[i] = v; }
int get(std::size_t i) const { return data[i]; }
std::size_t size() const { return count; }
};
int main() {
{
Buffer b(4);
b.set(0, 10);
b.set(3, 40);
std::cout << b.get(0) << " " << b.get(3) << " size=" << b.size() << "\n";
} // destructor runs here
std::cout << "scope ended\n";
return 0;
}
Buffer(4) allocated
10 40 size=4
Buffer(4) freed
scope ended
The constructor allocates, the destructor frees. Nobody using Buffer writes new or delete.
That is RAII stated properly: the resource's lifetime is the object's lifetime.
This Buffer is also, as written, dangerously broken. It works perfectly until someone copies
it — which is lesson 9.7, and the reason the Rule of Three exists.
Why the destructor is what makes exception safety possible
#include <iostream>
#include <stdexcept>
class Tracked {
const char* name;
public:
Tracked(const char* n) : name(n) { std::cout << " + " << name << "\n"; }
~Tracked() { std::cout << " - " << name << "\n"; }
};
void risky(bool fail) {
Tracked a("a");
Tracked b("b");
if (fail) {
throw std::runtime_error("boom");
}
std::cout << " no throw\n";
}
int main() {
std::cout << "normal:\n";
risky(false);
std::cout << "throwing:\n";
try {
risky(true);
} catch (const std::runtime_error& e) {
std::cout << "caught: " << e.what() << "\n";
}
return 0;
}
normal:
+ a
+ b
no throw
- b
- a
throwing:
+ a
+ b
- b
- a
caught: boom
Look at the throwing case: b and a are destroyed before the handler runs. That is stack
unwinding — as the exception propagates, every fully constructed local on the way out is
destroyed.
This is the deep reason RAII matters. Exception safety with manual cleanup is close to impossible, because every function call is a potential exit point. With destructors it is free.
Rules for writing one
Destructors must not throw. If a destructor throws while an exception is already propagating,
the program calls std::terminate. Destructors are implicitly noexcept in C++11 and later, so a
throw that escapes one ends the program. Catch and swallow inside if you must.
If you write a destructor, you almost certainly need more. A class with a destructor that does real work almost always needs a copy constructor and copy assignment too. That is the Rule of Three, and lesson 9.8 shows what breaks without it.
Most classes should not have one. If every member cleans up after itself — std::vector,
std::string, std::unique_ptr — the compiler-generated destructor is already correct. Writing
~MyClass() {} by hand when you did not need to is a small mistake with surprisingly large
consequences (lesson 9.11).
Try it
Add a Logger class whose constructor opens a "file" (just print) and whose destructor closes
it. Create one on the stack and one with new that you deliberately never delete. Compare
what gets printed.
Checkpoint
~ClassName()runs on destruction; no parameters, no return type, only one per class.- Stack objects are destroyed at their closing brace, in reverse construction order.
- Heap objects are destroyed only by
delete— or never. - During stack unwinding, every local on an exception's path out is destroyed.
- Destructors must not let exceptions escape; they are implicitly
noexcept. - Writing a destructor is a signal you also need copy operations (lesson 9.8).
- If every member manages itself, do not write a destructor at all.
Questions about this lesson
Log in to ask a question.