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.