Destructors & RAII
A constructor runs when an object is born. Its partner, the destructor, runs automatically when the object dies. This simple pair is behind one of the most important ideas in C++: RAII, short for "Resource Acquisition Is Initialization". It is the reason well-written C++ code rarely leaks memory, forgets to close files or leaves a lock locked, even though C++ has no garbage collector.
In this lesson you will see exactly when objects are destroyed, write a class that cleans up after itself, and learn the rules that tell you when you need to write a destructor at all.
Destructors and object lifetime
A destructor is named after the class with a tilde (~) in front. It takes no parameters and returns nothing. A local object is destroyed when execution leaves the block (the pair of braces) it was created in, and objects in the same block are destroyed in the reverse order of creation.
#include <print>
#include <string>
class Guest {
public:
explicit Guest(const std::string& name) : name_(name) {
std::println("{} arrives", name_);
}
~Guest() {
std::println("{} leaves", name_);
}
private:
std::string name_;
};
int main() {
Guest a("Aarav");
{
Guest b("Isha");
Guest c("Rohan");
std::println("-- inner block ends --");
}
Guest d("Meera");
std::println("-- main ends --");
}Aarav arrives Isha arrives Rohan arrives -- inner block ends -- Rohan leaves Isha leaves Meera arrives -- main ends -- Meera leaves Aarav leaves
Follow the timeline. Isha and Rohan leave as soon as the inner block closes, Rohan first because he arrived last. Meera and Aarav leave when main ends, again in reverse order. This is completely predictable, which is what makes RAII work: you always know the exact moment cleanup happens.
The problem RAII solves
Programs use resources that must be returned: memory, open files, network connections, locks. With C-style functions you must release each one by hand, on every path out of the function:
#include <cstdio>
bool save_report(const char* path, int total) {
std::FILE* f = std::fopen(path, "w");
if (f == nullptr) {
return false;
}
if (total < 0) {
return false; // BUG: returns without fclose, the file leaks
}
std::fprintf(f, "Total: %d\n", total);
std::fclose(f);
return true;
}Every early return, and every exception, is a chance to forget the cleanup. In a large program those small leaks add up until the program runs out of file handles or memory.
Writing an RAII class
The RAII idea: acquire the resource in a constructor and release it in the destructor. Because the destructor always runs when the object goes out of scope, cleanup can no longer be forgotten.
#include <cstdio>
#include <print>
class File {
public:
File(const char* path, const char* mode) : handle_(std::fopen(path, mode)) {
std::println("open {}", path);
}
~File() {
if (handle_ != nullptr) {
std::fclose(handle_);
std::println("closed");
}
}
File(const File&) = delete; // one owner only: no copies
File& operator=(const File&) = delete;
bool is_open() const { return handle_ != nullptr; }
std::FILE* get() const { return handle_; }
private:
std::FILE* handle_;
};
bool save_report(const char* path, int total) {
File f(path, "w");
if (!f.is_open()) {
return false;
}
if (total < 0) {
return false; // f is still closed automatically
}
std::println(f.get(), "Total: {}", total);
return true;
}
int main() {
save_report("report.txt", 4500);
save_report("report.txt", -1);
}open report.txt closed open report.txt closed
Both calls close the file, including the second one that returns early. Copying is deleted (both the copy constructor and the copy assignment, which you will meet in Operator Overloading), because two File objects closing the same handle would be a bug. Note also that C++23's std::println can write to a C FILE* as its first argument.
You already use RAII every day
You rarely need to write classes like File yourself, because the standard library is full of RAII types:
| Type | Resource it manages | Released when |
|---|---|---|
std::string, std::vector | Heap memory for the characters or items | The object is destroyed |
std::ofstream, std::ifstream | An open file | The stream is destroyed |
std::unique_ptr, std::shared_ptr | A heap object | The last owner is destroyed |
std::lock_guard, std::scoped_lock | A locked mutex | The guard is destroyed |
std::jthread | A running thread | It joins in its destructor |
#include <fstream>
#include <string>
void add_log_line(const std::string& line) {
std::ofstream log("app.log", std::ios::app); // file opened here
log << line << '\n';
} // file closed hereNo close() call is needed. You will use these types in File Streams, Smart Pointers vs new & delete and Threads, jthread & async.
The rule of zero, three and five
C++ can generate five special member functions for a class: the destructor, copy constructor, copy assignment, move constructor and move assignment. Three rules of thumb tell you when to write them:
- Rule of zero. If your class only holds members that already manage themselves (strings, vectors, smart pointers), write none of the five. The compiler's versions are correct. This should be the case for almost all your classes.
- Rule of three. If you must write a destructor because the class owns a raw resource, you almost certainly need to write or delete the copy constructor and copy assignment too, as
Filedoes. - Rule of five. In modern C++, add the move constructor and move assignment to that list, so ownership can be transferred cheaply. See Move Semantics.
Common mistakes
- Releasing resources by hand in modern code. If you write
fclose,deleteorunlockin normal code, look for an RAII type instead. - Copying a class that owns a raw resource. The default copy duplicates the handle, and both destructors release it. Delete or properly define copying.
- Expecting a destructor to run for objects created with
newand never deleted. Only automatic (local) objects and objects owned by RAII types are cleaned up for you. - Writing an empty destructor "just in case". It does nothing useful and can stop the compiler from generating fast move operations.
Key points
- A destructor
~Name()runs automatically when an object's lifetime ends; locals are destroyed in reverse order at the end of their block. - RAII ties a resource to an object: acquire in the constructor, release in the destructor.
- RAII makes cleanup happen on every path, including early returns and exceptions.
- Standard types such as
std::vector,std::ofstream,std::unique_ptrandstd::lock_guardare RAII types. - Follow the rule of zero; if you own a raw resource, handle copying and moving too (rules of three and five).
Exercise
Write an RAII class ScopeTimerLog that takes a task name, prints start: <name> in its constructor and end: <name> in its destructor. In main, create one for "exam", and inside a nested block create two more for "maths" and "physics". Predict the output before you run it.
Show solution
#include <print>
#include <string>
class ScopeTimerLog {
public:
explicit ScopeTimerLog(const std::string& task) : task_(task) {
std::println("start: {}", task_);
}
~ScopeTimerLog() {
std::println("end: {}", task_);
}
private:
std::string task_;
};
int main() {
ScopeTimerLog exam("exam");
{
ScopeTimerLog maths("maths");
ScopeTimerLog physics("physics");
}
}start: exam start: maths start: physics end: physics end: maths end: exam
The nested block ends first, destroying physics and then maths in reverse order; exam ends when main returns.
Quick Quiz
1. When is a local object's destructor called?
Automatic objects are destroyed at the end of their scope, in reverse order of creation.
2. What does RAII stand for?
RAII ties acquiring a resource to constructing an object, and releasing it to destroying the object.
3. Two objects x then y are created in the same block. In which order are they destroyed?
Objects in a block are destroyed in reverse order of construction.
4. According to the rule of zero, what should a class that holds only a std::string and a std::vector declare?
Its members already manage their own resources, so the compiler-generated functions are correct.
Learn C++ with a mentor
Free lessons get you started. Our Java, C & C++ Programming training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
