Skip to Content
Course content

33: Virtual Destructors

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was mentoring a junior dev who was working on a custom physics engine. He spent nearly two full days chasing a memory leak that only appeared during scene transitions. He was using Valgrind and seeing thousands of small allocations—specifically vertex buffers—that were "definitely lost." He was genuinely baffled because he had written a perfect destructor for his MeshShape class that cleaned up every single byte. The problem was that he was storing these shapes in a std::vector<Shape*> and calling delete on the base pointer. Because the base class destructor wasn't virtual, the MeshShape destructor was never even called. The program just deleted the tiny base Shape object and left the massive vertex buffers floating in memory.

The Danger of Base Pointer Deletion

In C++, when you delete a derived class object through a pointer to its base class, the behavior is undefined unless the base class has a virtual destructor. In practice, most compilers will simply call the base class destructor and ignore the derived one entirely. This is a silent killer because your code will compile without warnings, and your program will run normally—until you notice your RAM usage climbing steadily over time.

Consider this scenario. Imagine we have a base class for a UIElement and a derived ImageElement that allocates a buffer for pixel data:

class UIElement {
public:
    ~UIElement() { 
        // Base cleanup here
    }
};

class ImageElement : public UIElement {
    char* pixelData;
public:
    ImageElement() { pixelData = new char[1024]; }
    ~ImageElement() { 
        delete[] pixelData; 
        // This code is NEVER reached if deleted via UIElement*
    }
};

// The leak happens here:
UIElement* element = new ImageElement();
delete element; // Only ~UIElement() is called!

In the example above, pixelData is leaked every single time an ImageElement is deleted via a UIElement pointer. The compiler looks at the type of the pointer (UIElement*), sees a non-virtual destructor, and assumes it only needs to do the cleanup defined in the base class.

How Virtual Destructors Fix the Chain

Adding the virtual keyword to the base class destructor changes everything. It tells the compiler to look at the actual runtime type of the object (via the vtable) rather than just the pointer type. When you call delete on a base pointer with a virtual destructor, the program first finds the most derived destructor and executes it. Then, it automatically "walks up" the inheritance chain, calling the destructors of each base class in reverse order of construction.

Here is the corrected version:

class UIElement {
public:
    virtual ~UIElement() { 
        // Now the entry point for the cleanup chain
    }
};

class ImageElement : public UIElement {
    char* pixelData;
public:
    ImageElement() { pixelData = new char[1024]; }
    ~ImageElement() override { 
        delete[] pixelData; 
        // This now runs correctly!
    }
};

I've developed a rule of thumb over the years: if your class has even one virtual function, it must have a virtual destructor. If it's designed to be a base class, it must have a virtual destructor. There are some rare cases where you might want to avoid the overhead of a vtable, but in 99% of application code, the safety of a virtual destructor far outweighs the negligible performance cost.




📋 Practical Task

Fixing the Leaky AudioBuffer Manager

You have been handed a piece of a sound system where AudioClip objects are being leaked. The system manages different types of clips (Wave and MP3), but they are all stored in a master list of AudioClip* pointers. The WaveClip class allocates a heap buffer for raw PCM data, but this buffer is not being freed when the clips are deleted from the manager.

Your Task: Modify the provided code to ensure that when an AudioClip is deleted via a base pointer, the WaveClip destructor is properly triggered to free the pcmBuffer.

#include <iostream>

class AudioClip {
public:
    ~AudioClip() {
        std::cout < "Cleaning up base AudioClip\n";
    }
};

class WaveClip : public AudioClip {
    float* pcmBuffer;
public:
    WaveClip() {
        pcmBuffer = new float[100];
        std::cout < "Allocated Wave PCM buffer\n";
    }
    ~WaveClip() {
        delete[] pcmBuffer;
        std::cout < "Cleaning up Wave PCM buffer\n";
    }
};

int main() {
    AudioClip* myClip = new WaveClip();
    delete myClip; 
    return 0;
}

Expected Output after fix:
Allocated Wave PCM buffer
Cleaning up Wave PCM buffer
Cleaning up base AudioClip

Rating
0 0

There are no comments for now.

to be the first to leave a comment.