Abstract classes & pure virtual functions
Scenario
You properly use polymorphism to manage a list of `Animal*` pointers. At the end of the program, you call `delete` on each pointer. Later, your memory profiler reveals that the destructors for `Dog` and `Cat` never ran, causing massive memory leaks.
Why did deleting a base pointer fail to clean up the derived object?
Mental model
Deleting a base pointer is like throwing out a box labelled 'Animal'. If the destructor isn't `virtual`, the compiler doesn't check the vtable to see it's actually a `Dog`, so it only cleans up the generic Animal parts.
An abstract class is a class that defines an interface but cannot be instantiated directly. This is achieved by creating at least one 'pure virtual' function (`= 0`). The most critical rule of C++ inheritance is that if a class has ANY virtual functions (or is meant to be deleted through a base pointer), it absolutely MUST have a `virtual` destructor, otherwise destroying a derived object via a base pointer causes undefined behaviour (usually memory leaks).
Explanation
Sometimes a base class represents a concept that shouldn't exist on its own. For example, you can have a `Dog` or a `Cat`, but you shouldn't be able to instantiate a raw, generic `Animal`. In C++, you prevent instantiation by making the class 'abstract'.
You do this by adding `= 0` to a virtual function declaration: `virtual void makeSound() = 0;`. This is called a 'pure virtual function'. It forces any derived class to implement the function. If they don't, they also become abstract and cannot be instantiated. This is how C++ implements Interfaces.
**The Virtual Destructor Trap:**
When you use `new Dog()` but store it in an `Animal*`, calling `delete` on that pointer looks at the `Animal` class. If `~Animal()` is not virtual, the compiler uses static binding. It destroys the `Animal` part of the object and completely ignores the `Dog` part (so `Dog`'s specific memory is leaked).
**The Golden Rule:** If a class has at least one virtual function, its destructor MUST be virtual. Period.
Note that while `= 0` forces derived classes to override the function, you *can* still provide a default implementation for a pure virtual function outside the base class body, which derived classes can explicitly call if needed.
Code examples
The bug: Missing virtual destructor causes leaks
#include <iostream>
class Animal {
public:
// Pure virtual function makes Animal an Abstract Class
virtual void makeSound() const = 0;
// BUG: Non-virtual destructor
~Animal() { std::cout << "Animal destroyed\n"; }
};
class Dog : public Animal {
int* bones;
public:
Dog() : bones(new int[100]) {}
void makeSound() const override { std::cout << "Bark\n"; }
~Dog() {
delete[] bones;
std::cout << "Dog destroyed\n";
}
};
int main() {
Animal* a = new Dog();
a->makeSound();
// UB: Deleting through a base pointer with a non-virtual destructor.
// Only ~Animal() runs. ~Dog() is skipped. Memory leaks!
delete a;
return 0;
}
Because `~Animal()` is not virtual, the compiler does not look up the vtable when `delete a;` happens. The `Dog` destructor is entirely bypassed, and the 100 integers are leaked.
The fix: virtual destructor
#include <iostream>
class Animal {
public:
virtual void makeSound() const = 0;
// FIX: Virtual destructor ensures dynamic dispatch on delete
virtual ~Animal() { std::cout << "Animal destroyed\n"; }
};
class Dog : public Animal {
int* bones;
public:
Dog() : bones(new int[100]) {}
void makeSound() const override { std::cout << "Bark\n"; }
~Dog() override {
delete[] bones;
std::cout << "Dog destroyed\n";
}
};
int main() {
Animal* a = new Dog();
a->makeSound();
// Correctly routes to ~Dog(), which then automatically calls ~Animal()
delete a;
return 0;
}
Now, `delete a` consults the vtable, discovers the object is a `Dog`, and calls `~Dog()`. Once `~Dog()` finishes, it automatically chains up to `~Animal()`, cleanly destroying the entire object.
Key points
- Virtual functions demand virtual destructors: If your class has `virtual` anywhere, add a `virtual ~Class() = default;`.
- Interfaces = Pure Virtual: C++ models interfaces entirely through classes composed of pure virtual functions.
Common mistakes
- Providing an implementation for a pure virtual function inside the class body: Writing `virtual void func() = 0 { ... }` is illegal syntax. (You can provide a default implementation outside the class body, but it's rare and usually means the function shouldn't have been pure virtual).
- Forgetting that pure virtual functions make a class abstract: If a derived class forgets to `override` a single pure virtual function from its parent, the derived class silently remains abstract, resulting in confusing compiler errors when you try to instantiate it.
Recall questions
- What syntax is used to declare a pure virtual function in C++?
- What happens when you try to instantiate a class that contains a pure virtual function?
- Why MUST a base class with virtual functions have a virtual destructor?
Questions & answers
A candidate implements an interface class with several `= 0` methods but provides an empty standard destructor `~IReader() {}`. What feedback would you give?
The destructor must be declared `virtual`. If the interface is used to hold polymorphic objects via `IReader*`, calling `delete` on that pointer will cause undefined behaviour because it lacks dynamic dispatch for cleanup.
Approach: Connect interface design directly to the virtual destructor requirement.
Can you declare a pure virtual function but still provide an implementation for it?
Yes, you can provide an implementation outside the class definition (e.g., `void Animal::makeSound() { ... }`). Derived classes are still forced to override it, but they can explicitly call the base implementation if needed.
Approach: Know the obscure but valid edge case of pure virtual implementations.
Continue learning
Previous: Inheritance & virtual functions / vtable