C++ virtual destructors raise two key questions: when to use them and how they work. Let’s answer both.
Question 1: When should a destructor be virtual?
Answer: When deleting a derived class object through a base class pointer, the base class destructor must be virtual. This ensures the derived class destructor is called.
Reason: When calling a function through an object pointer, there are two cases: if the function is virtual, the derived class version is called; if it is non-virtual, the version matching the pointer’s declared type is called. Destructors follow the same rule. When an object goes out of scope or is deleted, its destructor is invoked. For a derived class object going out of scope, the derived destructor runs first, then the base destructor — this guarantees proper memory deallocation. However, if we delete a base class pointer pointing to a derived object and the base destructor is non-virtual, only the base destructor runs (case 2 above) — the derived destructor is never called, resulting in a partially destroyed object.
In simpler terms: destructors execute in derived-first, base-second order. If the destructor is non-virtual and we delete a derived dynamic object through a base class pointer, only the base destructor runs — the derived destructor is skipped, leaving the object incompletely destroyed.
Note: Not every class needs a virtual destructor. Declaring a virtual function adds a vtable (virtual function table) to the class, which stores virtual function pointers and increases the class’s memory footprint. Only make the destructor virtual when a class is intended to be used as a base class.
Example:
class Base {
public:
Base() { cout<<"Base Constructor"<<endl; }
// ~Base() { cout<<"Base Destructor"<<endl; }
virtual ~Base() { cout<<"Base Destructor"<<endl; }
};
class Derived: public Base{
public:
Derived() { cout<<"Derived Constructor"<<endl; }
~Derived() { cout<<"Derived Destructor"<<endl; }
};
int main(){
Base *p = new Derived();
delete p;
return 0;
}
Output without virtual destructor:
BaseConstructor
DerivedConstructor
BaseDestructor
Output with virtual destructor:
BaseConstructor
DerivedConstructor
DerivedDestructor
BaseDestructor
Question 2: How do virtual functions work in C++?
Virtual functions rely on the virtual function table (vtable). When a class has a function declared with the virtual keyword, a vtable is built to store the addresses of that class’s virtual functions. At the same time, the compiler adds a hidden pointer (the vtable pointer, or vptr) to the class, pointing to the vtable. If a derived class does not override a virtual function, its vtable entry still holds the base class function’s address. Whenever a virtual function is called, the vtable determines which concrete function to invoke. Thus, dynamic binding in C++ is achieved through the vtable mechanism. When we point a base class pointer at a derived class object, the vptr points to the derived class’s vtable, ensuring the derived class’s virtual functions are called.
Example:
#include <iostream>
using namespace std;
class Shape {
public:
Shape(){}
Shape(int edge_length){
this->edge_length = edge_length;
}
virtual ~Shape(){
cout<<"Shape destructure."<<endl;
}
virtual int circumstance(){
cout<<"circumstance of base class."<<endl;
return 0;
}
protected:
int edge_length;
};
class Triangle: public Shape{
public:
Triangle(){}
Triangle(int edge_length){
this->edge_length = edge_length;
}
~Triangle(){
cout<<"Triangle destructure."<<endl;
}
int circumstance(){
cout<<"circumstance of child class."<<endl;
return 3 * this->edge_length;
}
};
int main() {
Shape *x = new Shape();
x->circumstance();
Shape *y = new Triangle(10);
int num = y->circumstance();
cout<<num<<endl;
delete x;
delete y;
return 0;
}
Output:
circumstance of base class.
circumstance of child class.
30
Shape destructure.
Triangle destructure.
Shape destructure.