Destructors are the opposite of constructors. They’re special methods that are automatically called when an object goes out of scope. Similarly to constructors, destructors have no return type.

Basics

Destructors are typically defined automatically by the compiler, i.e., a default destructor doesn’t need to be explicitly defined. A default destructor can be defined with:

class Logger {
public:
	Logger(std::array<uint8_t, 256> buf);
	~Logger(void) = default;
};

Destructors should be defined by the developer if there are un-initialisation steps that must be handled before object deletion. For example, freeing heap memory (in C++), closing file handles/descriptors, or closing sockets. There should be explicit logic for these steps, lest we allow a resource leak.

Student::~Student() { // no return type
	if (grades != nullptr) {
		delete [] grades;
	}
}

Automatic calling of destructors when an object goes out of scope is a useful primitive. It builds the foundation for RAII (resource acquisition is initialisation).

For recursive data structures (like linked lists or binary trees), we can build a “chain reaction” for node deletions, where we call a destructor that calls a subsequent destructor, which allows us to delete an entire structure if we want to.

// in ComplexNumLL.hpp
~ComplexNumLL(void) {
	if (next != nullptr) delete next;
}
 
// typical usage
ComplexNumLL* n = new ComplexNumLL;
n->next = new ComplexNumLL;
delete p; // all clean!

Language-specific

In C++

C++ introduces virtual classes as a language feature. Here, abstract classes cannot be explicitly constructed, but concrete classes that inherit from them can be. The default behaviour for destructors of abstract classes is to be public and non-virtual.

Take the case where we have a pointer to a base class.

Base* basePtr = new Derived();
delete basePtr; // !!!

Deleting the base pointer won’t check the virtual function table and call the derived destructor. This only calls the base destructor. This can lead to a memory leak sometimes. The fix to this can take two forms:

class Base {
protected: // doesn't allow public calling of the destructor
		   // must be cast to derived type
	~Base(void);
};

The above prevents calling the base destructor by an unrelated class. The below will force a query through the virtual function table.

class Base {
public:
	virtual ~Base(void) = default;
}

In general, it’s recommended that the destructors in base classes are made virtual.