Abstract classes & pure virtual functions

RoadmapsC++

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

Common mistakes

Recall questions

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

Return to C++ Roadmap