C++
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C++
-
Section 4: Memory Management
-
Section 5: Templates and Generic Programming
-
Section 6: The Standard Template Library
-
Section 7: Modern C++ Features
-
Section 8: Error Handling
-
Section 9: Multithreading
-
Section 10: Operator Overloading and Type Conversion
-
Section 11: Advanced Topics
-
Section 12: Tooling and Build Systems
-
Section 13: Design Patterns in C++
-
Section 14: Interfacing with C and Systems Programming
-
Section 15: Networking and IPC Basics
-
Section 16: Graphics and Game Programming Basics
-
Section 17: The Boost Libraries
-
Section 18: Data Structures and Algorithms in C++
-
Section 19: Practical Projects
-
Section 20: More Concurrency Patterns
-
Section 21: More OOP and Design Practice
-
Section 22: File I/O and Streams
-
Section 23: More Standard Library
-
Section 24: Practice Exercises
-
Section 25: Interview and Algorithm Practice
-
Section 26: Compiler and Language Internals
-
Section 27: GUI and Application Frameworks Overview
-
Section 28: Testing and Quality Practices
-
Section 29: Numerics Library
-
Section 30: Concepts Library (C++20)
-
Section 31: Ranges Library (C++20) In Depth
-
Section 32: More Utility Library
33: Virtual Destructors
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
There are no comments for now.