Virtual Destructors

This chapter delves into the world of virtual destructors in C++, a critical concept for memory management in object-oriented programming. We'll explore the potential pitfalls of destructor inheritance and how virtual destructors ensure proper object cleanup within inheritance hierarchies.

Destructors - The Basics

What are Destructors?

Imagine a janitor cleaning up after everyone leaves the building. In C++, destructors are special member functions (similar to constructors) automatically called when an object goes out of scope or is explicitly deleted using the delete operator. Their purpose is to release any resources (like memory) allocated by the object during its lifetime.

Declaring a Destructor

Destructors have the same name as the class but prefixed with a tilde (~). They don’t have a return type and cannot take arguments.

				
					class MyClass {
public:
  // ... other member functions
  ~MyClass() {
    // Destructor code to release resources
  }
};

				
			

Automatic Destructor Call

When an object goes out of scope (e.g., at the end of a function or block), its destructor is automatically invoked to clean up its resources.

				
					void someFunction() {
  MyClass obj;  // Object created here
  // ... some code using obj
} // Destructor of obj is called automatically when the function exits

				
			

Explicit Destructor Call with delete

For dynamically allocated objects using new, you need to explicitly call the destructor using delete.

				
					MyClass* ptr = new MyClass;
// ... use ptr
delete ptr;  // Destructor of the object pointed to by ptr is called

				
			

The Problem with Destructors in Inheritance

Destructor Inheritance and Potential Issues

Consider a base class with a destructor and a derived class inheriting from it. When you delete a base class pointer pointing to a derived class object, only the base class destructor is called by default. This can lead to memory leaks if the derived class has its own resources to clean up.

Example of the Problem

				
					#include <iostream>
class Shape {
public:
  ~Shape() {
    std::cout << "Shape destructor called." << std::endl;
  }
};

class Circle : public Shape {
public:
  int radius;

  Circle(int radius) : radius(radius) {}

  ~Circle() {
    std::cout << "Circle destructor called." << std::endl;
  }
};

int main() {
  Shape* shapePtr = new Circle(5); // Base class pointer to derived class object
  delete shapePtr;
}


				
			
				
					// output //
Shape destructor called.
				
			

In this example, only the Shape destructor is called, leaving the memory allocated for the Circle object’s radius uncleaned (memory leak).

Virtual Destructors - The Solution

What are Virtual Destructors?

Virtual destructors are declared in the base class with the virtual keyword before the tilde (~virtual). Their purpose is to ensure that destructors of all classes in the inheritance hierarchy are called in the correct order during object deletion, starting from the most derived class and working its way up to the base class.

Declaring a Virtual Destructor

				
					class Shape {
public:
  virtual ~Shape() {
    std::cout << "Shape destructor called." << std::endl;
  }
};

				
			

Now, the Shape destructor is declared as virtual.

How Virtual Destructors Work

  • When you delete a base class pointer pointing to a derived class object, a virtual table (vtable) lookup happens at runtime.
  • The vtable stores information about the destructors for all virtual functions in the hierarchy, including the destructor.
  • The appropriate destructor for the actual object type is identified and called, ensuring proper cleanup in the correct order.

Revisited Example with Virtual Destructor

				
					class Shape {
public:
  virtual ~Shape() {
    std::cout << "Shape destructor called." << std::endl;
  }
};

class Circle : public Shape {
public:
  int radius;

  Circle(int radius) : radius(radius) {}

  ~Circle() {
    std::cout << "Circle destructor called." << std::endl;
  }
};

int main() {
  Shape* shapePtr = new Circle(5); // Base class pointer to derived class object
  delete shapePtr;
}

				
			
				
					// output //
Circle destructor called.
Shape destructor called.
				
			

With the virtual destructor in the Shape class, the destructors are now called in the correct order:

  1. Circle destructor: Releases memory for the radius member of the Circle object.
  2. Shape destructor: Executes any cleanup required by the base class Shape.

This ensures that all resources allocated by both the base and derived classes are properly deallocated, preventing memory leaks.

When to Use Virtual Destructors

Always Use Virtual Destructors in Base Classes

It’s considered good practice to declare virtual destructors in all base classes within an inheritance hierarchy, especially if the base class manages resources. This ensures proper object cleanup even if you don’t anticipate using base class pointers to derived class objects.

Exceptions

There might be rare cases where you know for sure that a base class will never have derived classes or doesn’t manage any resources. In such scenarios, you can avoid a virtual destructor in the base class for slight performance optimization (as virtual function calls have some overhead). However, this is an advanced use case, and generally, using virtual destructors in base classes is recommended for safety and maintainability.

Additional Notes

  • Remember that virtual destructors only affect the order of destructor calls during object deletion. They don’t affect the order of constructor calls during object creation.
  • Always consider using smart pointers like unique_ptr and shared_ptr for automatic memory management, especially when dealing with complex object ownership scenarios.

Virtual destructors are a fundamental concept in C++ object-oriented programming for ensuring proper memory management and preventing memory leaks within inheritance hierarchies. By understanding their functionality and importance, you can write robust and well-designed C++ code.Happy coding !❤️

Table of Contents