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 hereBecause 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:
- Vtable Lookup: The runtime looks up the destructor
address using the object's
vptr, which points toDerived's vtable. - 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). - Chain of Destruction: Once the derived destructor
finishes executing, it automatically calls the destructor of its direct
base class (
Base::~Base()). - Memory Deallocation: After the complete destructor
chain has finished, the operator
deletedeallocates 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).