C++ Destructors
Introduction to Destructors
Destructors in C++ are special member functions that are automatically called when an object’s lifetime ends (e.g., it goes out of scope) or when it is explicitly deleted. They perform cleanup operations and release resources acquired during the object’s lifetime.
Destructors are essential for preventing resource leaks and ensuring proper RAII (Resource Acquisition Is Initialization) in C++ programs.
Destructor Characteristics
- Destructors are named as the class name prefixed with a tilde (`~Class`).
- They cannot take parameters or return values, and a class can have only one destructor.
- They are automatically invoked when objects are destroyed; destruction order is the reverse of construction within an object.
- If you do not declare a destructor, one is implicitly declared. It may be implicitly defined as defaulted or as deleted depending on the destructibility of bases/members.
- Destructors are implicitly `noexcept` (cannot throw) unless a member/base destructor may throw. Never let exceptions escape a destructor.
Basic Destructor Example
This example shows a simple class with a destructor that prints a message when the object is destroyed.
#include <iostream>
using namespace std;
class MyClass {
public:
MyClass() {
cout << "Constructor called\n";
}
~MyClass() { // typically used for cleanup (files, sockets, etc.)
cout << "Destructor called\n";
}
};
int main() {
MyClass obj; // Constructor called
cout << "Inside main()\n";
return 0; // Destructor called automatically when obj goes out of scope
}
Constructor called Inside main() Destructor called
Destructor with Dynamic Memory (Rule of Five safe)
When managing raw dynamic memory, implement proper special member functions to avoid double frees and leaks (Rule of Five). Prefer standard containers or smart pointers when possible.
#include <iostream>
#include <cstddef>
using namespace std;
class ArrayWrapper {
private:
int* data{};
size_t size{};
public:
explicit ArrayWrapper(size_t s) : data(new int[s]{}), size(s) {
cout << "Array allocated with size: " << size << "\n";
}
~ArrayWrapper() {
delete[] data; // safe even if data is nullptr
cout << "Array deallocated\n";
}
// non-copyable (to avoid double delete); provide move support
ArrayWrapper(const ArrayWrapper&) = delete;
ArrayWrapper& operator=(const ArrayWrapper&) = delete;
ArrayWrapper(ArrayWrapper&& other) noexcept : data(other.data), size(other.size) {
other.data = nullptr; other.size = 0;
}
ArrayWrapper& operator=(ArrayWrapper&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data; size = other.size;
other.data = nullptr; other.size = 0;
}
return *this;
}
void setValue(size_t index, int value) { data[index] = value; }
int getValue(size_t index) const { return data[index]; }
};
int main() {
ArrayWrapper arr(5);
arr.setValue(0, 10);
cout << "Value at index 0: " << arr.getValue(0) << "\n";
}
Array allocated with size: 5 Value at index 0: 10 Array deallocated
Polymorphism & Virtual Destructors
When deleting through a base-class pointer, the base must have a virtual destructor so the derived destructor runs.
Without a virtual destructor, deleting a derived object via a base pointer calls only the base destructor → resource leaks.
#include <iostream>
using namespace std;
// No virtual destructor (bad for polymorphic deletion)
class BaseWithoutVirtual {
public:
~BaseWithoutVirtual() { cout << "BaseWithoutVirtual dtor\n"; }
};
class DerivedWithoutVirtual : public BaseWithoutVirtual {
public:
~DerivedWithoutVirtual() { cout << "DerivedWithoutVirtual dtor\n"; }
};
// Proper virtual destructor
class Base {
public:
virtual ~Base() { cout << "Base dtor\n"; }
};
class Derived : public Base {
public:
~Derived() override { cout << "Derived dtor\n"; }
};
int main() {
cout << "Without virtual destructor:\n";
BaseWithoutVirtual* p1 = new DerivedWithoutVirtual();
delete p1; // only base dtor runs
cout << "\nWith virtual destructor:\n";
Base* p2 = new Derived();
delete p2; // both derived then base
}
Without virtual destructor: BaseWithoutVirtual dtor With virtual destructor: Derived dtor Base dtor
Pure Virtual Destructors
An abstract base class can declare a pure virtual destructor to enforce abstractness, but it still must provide a definition (body).
#include <iostream>
struct Abstract {
virtual ~Abstract() = 0; // pure
};
inline Abstract::~Abstract() { std::cout << "Abstract dtor\n"; } // definition required
Exceptions and Destructors
Do not let exceptions escape from a destructor. During stack unwinding from another exception, throwing again calls `std::terminate()`.
If cleanup can fail, provide a separate `close()`/`flush()` member that reports errors; keep the destructor `noexcept`.
delete vs delete[]
`delete` must match `new`, and `delete[]` must match `new[]`. Using the wrong form is undefined behavior. `delete[]` calls the destructor for each element in the array.
Best Practices
- Define a destructor if your class manages resources (files, sockets, mutexes, memory). Better: wrap resources in RAII types (`std::unique_ptr`, `std::vector`, file/handle wrappers).
- Mark base-class destructors `virtual` when the class is intended to be used polymorphically.
- Follow the Rule of Three/Five/Zero: if you need any of destructor/copy/move operations, you likely need the others—or better, design so you need none (Rule of Zero).
- Keep destructors `noexcept`; handle failures via explicit functions.
- Avoid long/complex logic in destructors; keep them focused on releasing resources.
Summary Table
Scenario | Destructor Behavior |
---|---|
Object goes out of scope | Destructor automatically called |
`delete p` used | Destructor called, then memory freed |
`delete[] p` used for arrays | Destructor called for each element, then memory freed |
Exception thrown | Destructors run for all fully-constructed local objects during stack unwinding |
Virtual destructor on base | Ensures proper derived cleanup when deleting via base pointer |