Just starting with Destructors in C++? This guide makes it easy with simple analogies, clear code, and “Try it yourself” challenges after every section. Think of a destructor like closing up a shop at night: turn off the lights, lock the doors, and clean up resources so nothing leaks or breaks later.
In short, Destructors in C++ are special member functions that run automatically when an object’s lifetime ends (scope exit or deletion). Their job is to release resources—memory, files, sockets, handles—so programs stay efficient, safe, and leak‑free.
🔹 What is a Destructor in C++?
A destructor has the same name as the class, prefixed with a tilde (~), takes no parameters, returns nothing, and there can be only one per class. It runs automatically when the object goes out of scope or is deleted via a pointer. Analogy: when leaving a hotel room, the destructor is the act of returning the key, turning off AC, and ensuring everything is in order.
// Basics: destructor message on scope exit
#include <iostream>
using namespace std;
class Greeter {
public:
Greeter() { cout << "Greeter constructed\n"; }
~Greeter() { cout << "Greeter destroyed\n"; } // destructor
};
int main() {
{
Greeter g; // constructed here
cout << "Inside scope\n";
} // scope ends → destructor runs automatically
cout << "After scope\n";
}
Output
Greeter constructed
Inside scope
Greeter destroyed
After scope
Try it yourself
- Add another block scope and watch the order of “constructed/destroyed” messages change.
- Dynamically allocate with
new Greeter
and release withdelete
; observe destructor timing.
🔹 When Are Destructors in C++ Called?
- Automatic (stack) objects: when they go out of scope.
- Dynamic objects: when
delete
ordelete[]
is called on the pointer. - Containers: when the container itself is destroyed or elements are erased.
- Static/global objects: when the program terminates (order is implementation‑defined across translation units).
#include <iostream>
#include <vector>
using namespace std;
struct Tracer {
int id;
explicit Tracer(int i) : id(i)
{ cout << "Ctor " << id << "\n"; }
~Tracer() { cout << "Dtor " << id << "\n"; }
};
int main() {
Tracer a(1); // automatic
Tracer* p = new Tracer(2); // dynamic
{
vector<Tracer> v;
v.emplace_back(3);
// v elements destruct when v goes out of scope (reverse order)
v.emplace_back(4);
}
delete p; // calls destructor for id=2
}
Output
Ctor 1
Ctor 2
Ctor 3
Ctor 4
Dtor 4
Dtor 3
Dtor 2
Dtor 1
Try it yourself
- Push a few
Tracer
objects into astd::vector
, then callv.clear()
and observe destructor order. - Use
std::unique_ptr<Tracer>
instead of rawnew/delete
and verify auto cleanup.
🔹 Syntax and Rules of Destructors in C++
- One destructor per class; cannot be overloaded and has no parameters.
- Often implicitly
noexcept
; never throw exceptions from destructors. - Can be declared
virtual
in base classes to ensure proper polymorphic cleanup. - Can be
= default
(generated) if no special cleanup is needed.
struct Simple {
~Simple() = default; // let the compiler generate a trivial destructor
};
Try it yourself
- Create a class with a
= default
destructor; add a message in a manual destructor and compare behaviors. - Mark the destructor
noexcept
; try throwing in it (you’ll see why it’s discouraged).
🔹 Resource Management (RAII) with Destructors in C++
RAII means “Resource Acquisition Is Initialization”: acquire in constructors, release in destructors. It guarantees cleanup even if exceptions occur, making Destructors in C++ central to safe resource handling.
// File wrapper using RAII
#include <cstdio>
#include <iostream>
#include <stdexcept>
using namespace std;
class File {
FILE* fp{nullptr};
public:
File(const char* path, const char* mode) {
fp = fopen(path, mode);
if (!fp) throw runtime_error("Failed to open file");
}
~File() {
if (fp) {
fclose(fp); // cout << "File closed\n";
}
}
size_t write(const void* data, size_t sz)
{ return fwrite(data, 1, sz, fp); }
// non-copyable (simplest safety)
File(const File&) = delete;
File& operator=(const File&) = delete;
};
int main() {
try {
File f("out.txt", "w");
const char* msg = "Hello RAII\n";
f.write(msg, 11);
} catch (const exception& e) {
cerr << "Error: " << e.what() << "\n";
} // f's destructor closes the file even on exceptions
}
Output
No output
Try it yourself
- Wrap a network/socket handle or mutex in a similar RAII class with constructor acquire and destructor release.
- Force an exception between open and close to confirm automatic cleanup still happens.
🔹 Virtual Destructors and Polymorphism
If a class is intended for polymorphic use (deleted through a base pointer), its destructor must be virtual
. Otherwise, deleting via a base pointer may skip the derived destructor, causing leaks and undefined behavior.
#include <iostream>
using namespace std;
class Base {
public:
// virtual is crucial
virtual ~Base() { cout << "Base dtor\n"; }
};
class Derived : public Base {
int* data;
public:
Derived() : data(new int[3]{1,2,3}) {}
~Derived() override {
cout << "Derived dtor\n";
delete[] data;
}
};
int main() {
Base* p = new Derived();
delete p; // calls Derived::~Derived, then Base::~Base
}
Output
Derived dtor
Base dtor
Try it yourself
- Remove
virtual
fromBase
’s destructor and observe how only the base destructor runs (don’t keep this in real code!). - Add another layer (e.g.,
MoreDerived
) to see the full chain of virtual destruction.
🔹 Order of Destruction (Members, Derived, Base)
Destruction happens in reverse of construction. For a derived object: first the most‑derived destructor body runs, then its members are destroyed (reverse declaration order), and finally base class subobjects are destroyed (reverse base order).
#include <iostream>
using namespace std;
struct Part {
const char* name;
explicit Part(const char* n) : name(n)
{ cout << "Ctor " << name << "\n"; }
~Part() { cout << "Dtor " << name << "\n"; }
};
struct Base {
Base() { cout << "Base ctor\n"; }
~Base() { cout << "Base dtor\n"; }
};
struct Car : Base {
Part engine{"engine"};
Part wheel{"wheel"};
Car() { cout << "Car ctor\n"; }
~Car() { cout << "Car dtor\n"; }
};
int main() {
Car c;
}
/* Construct: Base ctor → engine → wheel → Car ctor
Destruct: Car dtor → wheel → engine → Base dtor */
Output
Base ctor
Ctor engine
Ctor wheel
Car ctor
Car dtor
Dtor wheel
Dtor engine
Base dtor
Try it yourself
- Swap the declaration order of
engine
andwheel
and observe how destruction order changes accordingly. - Add another base class and print messages to see multi‑base destruction order.
🔹 Exceptions and Destructors in C++
Best practice: do not let exceptions escape a destructor. If a destructor throws during stack unwinding (another exception is already active), the program will call std::terminate
. Prefer logging and swallowing errors or provide an explicit close()
method that can report failure.
#include <iostream>
#include <exception>
using namespace std;
struct Cleaner {
~Cleaner() noexcept {
try {
// riskyCleanup();
} catch (...) {
// log and suppress; never throw from a destructor
cerr << "Cleanup failed but suppressed in destructor\n";
}
}
};
int main() {
try {
Cleaner c;
throw runtime_error("oops");
} catch (const exception& e) {
cout << e.what() << "\n";
}
}
Output
oops
🔹 Smarter Cleanup: Prefer Smart Pointers Over Manual Destructors
Smart pointers (std::unique_ptr
, std::shared_ptr
) automatically delete managed memory in their own destructors. This reduces the need to write custom destructors and helps avoid leaks and double frees.
#include <memory>
#include <vector>
#include <iostream>
using namespace std;
struct Payload {
int v;
explicit Payload(int x) : v(x)
{ cout << "Payload " << v << " constructed\n"; }
~Payload() { cout << "Payload " << v << " destroyed\n"; }
};
int main() {
vector<unique_ptr<Payload>> items;
items.push_back(make_unique<Payload>(1));
items.push_back(make_unique<Payload>(2));
// All Payloads auto-destroy when 'items' goes out of scope
}
Output
Payload 1 constructed
Payload 2 constructed
Payload 2 destroyed
Payload 1 destroyed
🔹 Rule of Three/Five and Destructors in C++
If a class manages a resource, provide proper copy/move operations and a destructor. Rule of Three (destructor, copy constructor, copy assignment). Modern C++ adds Rule of Five (move constructor and move assignment) for efficiency. If no resources are owned, rely on defaults (Rule of Zero).
#include <cstddef>
#include <algorithm>
class Buffer {
size_t n{};
int* data{};
public:
Buffer() = default;
explicit Buffer(size_t size) : n(size), data(new int[size]{}) {}
// Copy constructor
Buffer(const Buffer& other) : n(other.n),
data(new int[other.n]{}) {
std::copy(other.data, other.data + n, data);
}
// Copy assignment
Buffer& operator=(const Buffer& other) {
if (this != &other) {
int* tmp = new int[other.n]{};
std::copy(other.data, other.data + other.n, tmp);
delete[] data;
data = tmp;
n = other.n;
}
return *this;
}
// Move constructor
Buffer(Buffer&& other) noexcept : n(other.n),
data(other.data) {
other.n = 0;
other.data = nullptr;
}
// Move assignment
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data;
n = other.n;
data = other.data;
other.n = 0;
other.data = nullptr;
}
return *this;
}
// Destructor
~Buffer() { delete[] data; }
};
Try it yourself
- Add logging to each special member to visualize copies vs moves in different scenarios.
- Benchmark pushing
Buffer
intostd::vector
with and without moves.
🔹 Mini Project: ScopedTimer — Cleanup Work in the Destructor
This small utility uses a destructor to automatically report elapsed time when it goes out of scope. It’s a practical example of using Destructors in C++ for helpful side effects at scope end.
#include <chrono>
#include <iostream>
#include <string>
#include <utility>
using namespace std;
class ScopedTimer {
string label;
chrono::steady_clock::time_point start;
public:
explicit ScopedTimer(string name = "Timer") :
label(move(name)), start(chrono::steady_clock::now()) {}
~ScopedTimer() {
using namespace chrono;
auto ms = duration_cast<milliseconds>(steady_clock::now() - start).count();
cout << "[" << label << "]
elapsed: " << ms << " ms\n";
}
};
int main() {
{ ScopedTimer t("Loading"); /* simulate work... */ }
// prints elapsed time automatically
}
Output
[Loading] elapsed: 0 ms
Try it yourself
- Add a constructor flag to mute output; ensure the destructor respects it.
- Wrap a function with
ScopedTimer
and compare different algorithm runtimes.
🔹 Best Practices for Destructors in C++
- Never let exceptions escape destructors; log and suppress or use explicit
close()
APIs for error reporting. - Make base class destructors
virtual
when using polymorphism. - Prefer smart pointers and RAII; avoid raw
new/delete
where possible. - Follow Rule of Three/Five for resource‑owning classes.
- Keep destructors lightweight; avoid heavy work or blocking operations.
🔹 Common Pitfalls to Avoid
- Throwing exceptions from destructors (can terminate the program during unwinding).
- Omitting
virtual
in base destructors used polymorphically. - Forgetting to release all resources (files, sockets, memory, mutexes).
- Relying on destructor order across translation units (global/static) — it’s implementation‑defined.
- Manual memory management without considering smart pointers.
🔹 FAQs: Destructors in C++
Q1. Can a destructor be overloaded or take parameters?
No. There’s exactly one destructor per class, it takes no parameters, and has no return type.
Q2. When do I need a custom destructor?
When the class owns resources that require explicit release (heap memory, files, sockets, handles). Otherwise, the default destructor is fine.
Q3. Should destructors be virtual?
Yes for base classes intended for polymorphic deletion via base pointers. Otherwise, virtual isn’t required.
Q4. Is it safe to throw exceptions from destructors?
Avoid it. Throwing during stack unwinding causes std::terminate
. Prefer logging and non‑throwing cleanup.
Q5. How do smart pointers relate to destructors?
Smart pointers manage deletion in their own destructors, ensuring cleanup without manual delete
. This reduces the need for custom destructors in many classes.
Q6. What’s the destruction order for composed and inherited objects?
For a derived object: derived destructor body → members (reverse declaration order) → base destructors (reverse base order).
🔹 Wrapping Up
Mastering Destructors in C++ is key to writing safe, leak‑free, and maintainable code. Use RAII, favor smart pointers, keep destructors lightweight and non‑throwing, and make base destructors virtual when using polymorphism. Practice the “Try it yourself” tasks to turn good cleanup habits into second nature.