Virtual Destructors in C++: Ensuring Complete Cleanup

In C++, deleting a dynamically allocated derived class object through a base class pointer can lead to resource leaks and undefined behavior if the base class destructor is not declared virtual. This article explains how virtual destructors leverage dynamic dispatch and the virtual method table (vtable) to guarantee that destructors are called in the correct order—from the most-derived class up to the base class—ensuring total resource cleanup.

The Problem: Static Binding Without Virtual Destructors

When you delete an object through a pointer, the C++ compiler must determine which destructor to execute. By default, member function calls in C++ use static binding (compile-time resolution).

Consider the following scenario:

class Base {
public:
    ~Base() { /* Base cleanup */ }
};

class Derived : public Base {
    int* data;
public:
    Derived() : data(new int[100]) {}
    ~Derived() { delete[] data; }
};

Base* ptr = new Derived();
delete ptr; // Problem occurs here

Because ptr is of type Base* and ~Base() is non-virtual, the compiler statically resolves the call to Base::~Base(). The Derived destructor is completely skipped. As a result, the dynamically allocated memory managed by Derived::data is never freed, leading to a memory leak. Furthermore, the C++ standard states that deleting a derived object through a base pointer with a non-virtual destructor results in undefined behavior.

The Solution: Dynamic Dispatch via Virtual Destructors

Declaring the base class destructor as virtual instructs the compiler to use dynamic binding (runtime resolution) rather than static binding.

class Base {
public:
    virtual ~Base() { /* Base cleanup */ }
};

When a class defines or inherits a virtual function, the compiler generates a virtual method table (vtable) for that class and adds a hidden pointer (the vptr) to the object instance. The vptr points to the vtable corresponding to the actual runtime type of the object.

When delete ptr; is invoked on a Base* pointing to a Derived instance:

  1. Vtable Lookup: The runtime looks up the destructor address using the object's vptr, which points to Derived's vtable.
  2. Derived Destructor Execution: The lookup resolves to Derived::~Derived(), allowing the derived class to release its specific resources (such as dynamically allocated memory, file handles, or network sockets).
  3. Chain of Destruction: Once the derived destructor finishes executing, it automatically calls the destructor of its direct base class (Base::~Base()).
  4. Memory Deallocation: After the complete destructor chain has finished, the operator delete deallocates the raw memory occupied by the entire object.

The General Rule

Any class designed to serve as a polymorphic base class—meaning it contains at least one virtual function or is intended to be manipulated via a pointer to the base—should declare a public virtual destructor. Conversely, if a class is not meant to be used polymorphically or deleted via a base pointer, its destructor can remain non-virtual (or be declared protected and non-virtual to prevent deletion through a base pointer entirely).