C++ Destructors

A destructor is a special member function that runs automatically when an object’s lifetime ends, giving you a guaranteed place to release resources the object was holding — dynamically allocated memory, file handles, network sockets, locks, and more. Every class has one, whether you write it yourself or let the compiler generate it. Understanding destructors is the key to writing C++ that never leaks memory and never crashes from double-freeing or dangling pointers, and it underlies the whole RAII (Resource Acquisition Is Initialization) philosophy that makes C++ resource management safe.

Overview / How Destructors Work

Every object in C++ goes through two phases: construction (birth) and destruction (death). The constructor runs when the object is created; the destructor runs when the object is destroyed. Destruction happens in one of these situations:

  • A local (stack) object goes out of scope — when the enclosing { } block ends.
  • A dynamically allocated object is explicitly destroyed with delete (for a single object) or delete[] (for an array).
  • A member object is destroyed automatically as part of its owning object’s destruction.
  • A temporary object’s lifetime ends (usually at the end of the full expression that created it).
  • The program terminates, destroying any remaining global/static objects.

Internally, the compiler inserts a call to the destructor at each of these points. For stack objects, this happens automatically as part of unwinding the function’s stack frame — the compiler knows the exact point in the generated machine code where the object’s scope ends and inserts the destructor call there. Objects are destroyed in the reverse order of their construction (last constructed, first destroyed), which mirrors how a stack works. This matters when several local objects depend on each other: the one that was set up last is torn down first, before its dependencies disappear.

If a class has member variables that are themselves objects (not raw pointers), those members’ destructors are invoked automatically after the class’s own destructor body finishes, again in reverse order of declaration. This is one reason to prefer using standard containers and smart pointers as members over raw pointers: their destructors already know how to clean themselves up, so you often don’t need to write a destructor at all.

If you don’t declare a destructor, the compiler generates an implicit one that simply destroys each member in turn. This implicit destructor is often enough for classes that don’t manage raw resources directly (memory obtained with new, file handles from the C API, raw OS handles). But if your class does own such a resource, you must write a destructor to release it, or you will leak that resource every time an object of that class is destroyed.

Syntax

A destructor has a very restrictive syntax: it is named after the class with a tilde (~) in front, takes no parameters, returns nothing (not even void), and there can only ever be one per class — it cannot be overloaded.

class ClassName {
public:
    ~ClassName() {
        // release resources here
    }
};
Part Meaning
~ClassName Name is always the class name prefixed with a tilde.
No parameters A destructor never takes arguments, so it cannot be overloaded.
No return type Not even void is written.
virtual (optional) Marks the destructor as overridable; required in any class meant to be used polymorphically as a base class.
Called automatically You almost never call a destructor directly — the compiler invokes it for you.

Examples

Example 1: Construction and destruction order

#include <iostream>
using namespace std;

class Logger {
public:
    string name;
    Logger(string n) : name(n) {
        cout << "Constructing " << name << endl;
    }
    ~Logger() {
        cout << "Destructing " << name << endl;
    }
};

int main() {
    Logger a("A");
    {
        Logger b("B");
        cout << "Inside inner scope" << endl;
    }
    cout << "Back in main" << endl;
    return 0;
}

Output:

Constructing A
Constructing B
Inside inner scope
Destructing B
Back in main
Destructing A

Object b is created inside the inner block, so it is destroyed the instant that block ends — well before a, even though a was created first. This demonstrates the reverse-order, scope-based destruction rule that C++ guarantees.

Example 2: RAII — managing dynamic memory automatically

#include <iostream>
using namespace std;

class IntArray {
private:
    int* data;
    int size;
public:
    IntArray(int s) : size(s) {
        data = new int[size];
        for (int i = 0; i < size; i++) data[i] = i * i;
        cout << "Allocated array of size " << size << endl;
    }
    ~IntArray() {
        delete[] data;
        cout << "Freed array of size " << size << endl;
    }
    void print() const {
        for (int i = 0; i < size; i++) cout << data[i] << " ";
        cout << endl;
    }
};

int main() {
    IntArray arr(5);
    arr.print();
    return 0;
}

Output:

Allocated array of size 5
0 1 4 9 16 
Freed array of size 5

The constructor allocates a raw array with new[]; the destructor releases it with delete[]. Because the destructor runs automatically when arr goes out of scope, the caller never has to remember to free anything — this pairing of “acquire in constructor, release in destructor” is exactly what RAII means.

Example 3: Virtual destructors and polymorphism

#include <iostream>
using namespace std;

class Animal {
public:
    Animal() { cout << "Animal created" << endl; }
    virtual ~Animal() { cout << "Animal destroyed" << endl; }
};

class Dog : public Animal {
private:
    int* records;
public:
    Dog() : records(new int[10]) {
        cout << "Dog created" << endl;
    }
    ~Dog() override {
        delete[] records;
        cout << "Dog destroyed" << endl;
    }
};

int main() {
    Animal* pet = new Dog();
    delete pet;
    return 0;
}

Output:

Animal created
Dog created
Dog destroyed
Animal destroyed

Even though pet is declared as Animal*, delete pet correctly runs Dog‘s destructor first, then Animal‘s. This only works because Animal‘s destructor is marked virtual, which tells the compiler to look up the actual (dynamic) type of the object at runtime instead of blindly calling the destructor that matches the pointer’s declared (static) type.

How It Works Step by Step / Under the Hood

  • Non-virtual destructors are resolved at compile time, purely from the pointer’s declared type. If you delete a derived object through a base pointer and the base destructor is not virtual, only the base part of the object is destroyed — the derived class’s destructor never runs.
  • Virtual destructors work like any other virtual function: the class has a hidden vtable (virtual function table), and each object stores a pointer to it. When you call delete through a base pointer, the program follows the vtable pointer at runtime to find the actual most-derived destructor, runs it, and then that destructor automatically chains up to call its base class’s destructor, and so on up the hierarchy.
  • Destructor chaining is automatic and cannot be skipped: you never write code to call a base class destructor yourself — the compiler always inserts that call at the end of every destructor body.
  • Array destruction with delete[] calls the destructor once for every element in the array, in reverse index order, before the compiler frees the underlying memory block.
  • Exceptions: if a destructor throws while the program is already unwinding the stack due to another exception, std::terminate is called and the program aborts immediately — this is why destructors should never throw.

Common Mistakes

Mistake 1: Forgetting a virtual destructor in a polymorphic base class

#include <iostream>
using namespace std;

class Shape {
public:
    Shape() { cout << "Shape constructed" << endl; }
    ~Shape() { cout << "Shape destroyed" << endl; } // not virtual!
};

class Circle : public Shape {
private:
    double* radiusHistory;
public:
    Circle() {
        radiusHistory = new double[10];
        cout << "Circle constructed" << endl;
    }
    ~Circle() {
        delete[] radiusHistory;
        cout << "Circle destroyed" << endl;
    }
};

int main() {
    Shape* s = new Circle();
    delete s;
    return 0;
}

Output:

Shape constructed
Circle constructed
Shape destroyed

Notice "Circle destroyed" never prints — radiusHistory is leaked, and technically the behavior is undefined because the standard requires the static and dynamic types to match when deleting through a non-virtual destructor. The fix is simply to mark the base destructor virtual:

#include <iostream>
using namespace std;

class Shape {
public:
    Shape() { cout << "Shape constructed" << endl; }
    virtual ~Shape() { cout << "Shape destroyed" << endl; }
};

class Circle : public Shape {
private:
    double* radiusHistory;
public:
    Circle() {
        radiusHistory = new double[10];
        cout << "Circle constructed" << endl;
    }
    ~Circle() override {
        delete[] radiusHistory;
        cout << "Circle destroyed" << endl;
    }
};

int main() {
    Shape* s = new Circle();
    delete s;
    return 0;
}

Output:

Shape constructed
Circle constructed
Circle destroyed
Shape destroyed

Rule of thumb: if a class has even one virtual function, or is ever meant to be deleted through a base pointer, give it a virtual destructor.

Mistake 2: Managing a raw resource without writing a destructor

#include <iostream>
using namespace std;

class Buffer {
private:
    int* data;
    int size;
public:
    Buffer(int s) : size(s) {
        data = new int[size];
        cout << "Buffer allocated" << endl;
    }
    // No destructor -- the memory from 'new int[size]' is never freed!
};

int main() {
    for (int i = 0; i < 3; i++) {
        Buffer b(1000);
    }
    cout << "Done" << endl;
    return 0;
}

Output:

Buffer allocated
Buffer allocated
Buffer allocated
Done

The program compiles and runs without any visible error, but each of the three Buffer objects leaks 1000 ints when it goes out of scope, because the compiler-generated destructor only knows how to destroy member objects — it has no idea that data points at heap memory it should free. The fix is to add a destructor that explicitly releases the resource:

#include <iostream>
using namespace std;

class Buffer {
private:
    int* data;
    int size;
public:
    Buffer(int s) : size(s) {
        data = new int[size];
        cout << "Buffer allocated" << endl;
    }
    ~Buffer() {
        delete[] data;
        cout << "Buffer freed" << endl;
    }
};

int main() {
    for (int i = 0; i < 3; i++) {
        Buffer b(1000);
    }
    cout << "Done" << endl;
    return 0;
}

Output:

Buffer allocated
Buffer freed
Buffer allocated
Buffer freed
Buffer allocated
Buffer freed
Done

This is exactly the pattern behind the “Rule of Three”: if your class defines a destructor, it very likely also needs a custom copy constructor and copy assignment operator, because the default (memberwise) copies would otherwise copy the raw pointer and cause two objects to try to free the same memory.

Best Practices

  • Prefer smart pointers (std::unique_ptr, std::shared_ptr) and standard containers over raw new/delete so you rarely need to write a destructor at all.
  • Always make a base class destructor virtual if the class has any other virtual function or is ever deleted through a base class pointer.
  • Never let an exception escape a destructor — mark destructors noexcept (the default in modern C++) and catch anything that might throw inside them.
  • Follow the Rule of Three/Five: if you write a destructor, copy constructor, or copy assignment operator, you almost always need all of them (and consider move operations too).
  • Keep destructors focused purely on cleanup — don’t put business logic there that has side effects beyond releasing resources.
  • Never call a destructor explicitly (e.g. obj.~ClassName()) unless you are working with placement new; doing so on a normal object causes it to be destroyed twice.

Practice Exercises

  • Write a Logger class that prints a message in its constructor and destructor. Create three Logger objects in nested scopes and predict the destruction order before running it.
  • Write a DynamicString class that allocates a char* buffer with new[] in its constructor and frees it with delete[] in its destructor, printing a message each time. Create several objects inside a loop and confirm each one is freed.
  • Start from the non-virtual Shape/Circle example above, delete a Circle through a Shape*, and observe that "Circle destroyed" is missing. Then add virtual to the base destructor and confirm the output changes.

Summary

  • A destructor (~ClassName()) runs automatically when an object’s lifetime ends, giving you a guaranteed cleanup point.
  • Destructors take no parameters, return nothing, and cannot be overloaded — there is exactly one per class.
  • Local objects are destroyed in reverse order of construction when their scope ends; this underlies the RAII pattern.
  • If a class manages a raw resource (memory, file handle, etc.), it needs a destructor that releases it, or the resource leaks.
  • Base classes used polymorphically must declare their destructor virtual, or deleting through a base pointer skips the derived class’s cleanup.
  • Destructors should never throw exceptions, and should never be called explicitly on normally-constructed objects.