Destructors — Resource Cleanup and the RAII Concept
In this tutorial, you will learn about Destructors. We cover key concepts, practical examples, and best practices to help you master this topic.
C++ destructors are invoked automatically when objects go out of scope, providing deterministic cleanup that makes RAII the cornerstone of correct resource management in C++.
What You'll Learn
You will write destructors that release resources like heap memory, file handles, and mutex locks, understand the RAII idiom and why it eliminates resource leaks, recognize the destructor invocation order for class members and base classes, and learn the rules for virtual destructors in polymorphic classes.
Why It Matters
In languages with Garbage Collection, you do not think about when objects are destroyed. In C++, destruction is deterministic and happens at a precise point: when the object goes out of scope. This determinism is the foundation of RAII, which ties resource lifetimes to object lifetimes. RAII is why C++ code can be exception-safe without try/finally blocks.
Learning Path
graph LR
A["12: Constructors"] --> B["13: Destructors"]
B --> C["14: Encapsulation"]
C --> D["15: Inheritance"]
style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
style D fill:#4a90d9,stroke:#2c5f8a,color:#fff
The Destructor
#include <iostream>
class Resource {
private:
int id_;
public:
Resource(int id) : id_(id) {
std::cout << "Acquire resource " << id_ << "\n";
}
~Resource() {
std::cout << "Release resource " << id_ << "\n";
}
};
int main() {
{
Resource r1(1);
Resource r2(2);
std::cout << "Inside block\n";
} // r2 destroyed first, then r1 (reverse order)
std::cout << "After block\n";
}
Expected output:
Acquire resource 1
Acquire resource 2
Inside block
Release resource 2
Release resource 1
After block
Destructors:
- Have the same name as the class prefixed with
~ - Take no arguments
- Have no return type
- Cannot be overloaded (only one destructor per class)
- Are called automatically when an object goes out of scope
- Are called in reverse order of construction
RAII: Resource Acquisition Is Initialization
RAII ties resource lifetime to object lifetime. Acquire the resource in the constructor, release it in the destructor.
#include <iostream>
#include <stdexcept>
class FileHandle {
private:
FILE* file_;
public:
FileHandle(const char* filename, const char* mode) {
file_ = std::fopen(filename, mode);
if (!file_) {
throw std::runtime_error("Cannot open file");
}
std::cout << "File opened\n";
}
~FileHandle() {
if (file_) {
std::fclose(file_);
std::cout << "File closed\n";
}
}
void write(const char* text) {
std::fputs(text, file_);
}
// Delete copy operations
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
};
int main() {
try {
FileHandle f("test.txt", "w");
f.write("Hello RAII\n");
// No explicit close needed!
} catch (const std::exception& e) {
std::cout << e.what() << "\n";
}
// File is closed even if an exception occurs
}
Without RAII, every code path that uses a resource must explicitly release it, including error paths. With RAII, the destructor runs automatically when the scope is exited, whether normally or via exception.
Destructor Order
#include <iostream>
struct Member {
int id;
Member(int i) : id(i) { std::cout << "Member " << id << " constructed\n"; }
~Member() { std::cout << "Member " << id << " destroyed\n"; }
};
struct Container {
Member m1{1};
Member m2{2};
Member m3{3};
~Container() {
std::cout << "Container destroyed\n";
}
};
int main() {
Container c;
}
Expected output:
Member 1 constructed
Member 2 constructed
Member 3 constructed
Container destroyed
Member 3 destroyed
Member 2 destroyed
Member 1 destroyed
Destruction order is the reverse of construction:
- Destructor body runs
- Member destructors run in reverse declaration order
- Base class destructors run (for derived classes)
Virtual Destructors
When deleting a derived class object through a base class pointer, the destructor must be virtual to ensure the derived destructor runs.
#include <iostream>
class Base {
public:
Base() { std::cout << "Base constructed\n"; }
virtual ~Base() { std::cout << "Base destroyed\n"; }
};
class Derived : public Base {
private:
int* data_;
public:
Derived() : data_(new int[100]) {
std::cout << "Derived constructed\n";
}
~Derived() override {
delete[] data_;
std::cout << "Derived destroyed\n";
}
};
int main() {
Base* ptr = new Derived();
delete ptr; // calls Derived destructor then Base destructor (with virtual)
}
Without virtual ~Base(), deleting through Base* would only call ~Base(), leaking Derived's resources.
Rule: If a class has any virtual function, it should have a virtual destructor.
Pure Virtual Destructor
#include <iostream>
class AbstractBase {
public:
virtual ~AbstractBase() = 0; // pure virtual destructor
};
AbstractBase::~AbstractBase() {
// Must provide a body even though it is pure virtual
}
class Concrete : public AbstractBase {
public:
~Concrete() override {
std::cout << "Concrete destroyed\n";
}
};
int main() {
Concrete c;
}
A pure virtual destructor is a way to make a class abstract without any other pure virtual function. It still needs a body because derived class destructors call it.
Common Mistakes
Mistake 1: Non-Virtual Destructor in Base Class
class Base { ~Base(); }; // non-virtual
class Derived : public Base { ~Derived(); };
Base* p = new Derived();
delete p; // undefined behavior: only ~Base() called
Mistake 2: Throwing in Destructors
~MyClass() {
throw std::runtime_error("boom"); // BAD
}
Destructors must not throw. If a destructor throws during stack unwinding (when another exception is active), std::terminate is called. Always handle errors in destructors internally.
Mistake 3: Double Delete from Shallow Copy
If you use the compiler-generated copy constructor with a raw pointer member, two objects share the same pointer, and both will try to delete it. Use smart pointers or implement the rule of three/five.
Mistake 4: Forgetting to Call Base Destructor
Base class destructors are called automatically. Do not call them explicitly.
Mistake 5: Relying on Destructors for Non-Deterministic Cleanup
void func() {
static Resource r; // destroyed at program exit, not at end of func
}
Static and global objects are destroyed at program termination, not when leaving a scope.
Practice Questions
- In what order are members destroyed? Why does this order matter?
- Why must destructors not throw exceptions?
- What happens if you delete a derived object through a base pointer without a virtual destructor?
- Write a
MutexLockRAII class that acquires a mutex in the constructor and releases it in the destructor. - Can a destructor be
= delete? When would you want that?
Challenge
Implement a ScopedTimer class that prints the elapsed time since construction when it goes out of scope. Use <chrono> for timing. Demonstrate it with a function that does some work (e.g., a loop).
FAQ
Mini Project
Build a HeapArray RAII class:
#include <iostream>
class HeapArray {
private:
int* data_;
size_t size_;
public:
HeapArray(size_t size) : data_(new int[size]()), size_(size) {
std::cout << "Allocated " << size << " ints\n";
}
~HeapArray() {
delete[] data_;
std::cout << "Deallocated " << size_ << " ints\n";
}
HeapArray(const HeapArray&) = delete;
HeapArray& operator=(const HeapArray&) = delete;
int& operator[](size_t i) { return data_[i]; }
const int& operator[](size_t i) const { return data_[i]; }
size_t size() const { return size_; }
};
int main() {
HeapArray arr(10);
for (size_t i = 0; i < arr.size(); ++i) {
arr[i] = static_cast<int>(i * i);
}
for (size_t i = 0; i < arr.size(); ++i) {
std::cout << arr[i] << " ";
}
std::cout << "\n";
} // arr destructor runs here, freeing memory automatically
What's Next
Destructors and RAII are the foundation of resource safety in C++. The next lesson covers Encapsulation in depth: public, private, protected specifiers, friend functions, and friend classes.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro