C++

Chapter 16: C++ Polymorphism — Virtual Functions and Abstract Classes

By Ali Naqi • December 09, 2025

Chapter 16: C++ Polymorphism — Virtual Functions and Abstract Classes

Chapter 16: Polymorphism—Virtual Functions and Abstract Classes

Welcome to the final and most powerful pillar of Object-Oriented Programming: **Polymorphism**. The word comes from Greek, meaning "many forms." In programming, it describes the ability of different objects to respond to the same message (function call) in their own unique way.

In Chapter 15, we created a Dog that inherited from Animal. We overrode the makeSound() function so the dog could bark. However, there is a massive hidden trap in standard C++ inheritance that we haven't discussed yet. If you point to a Dog using an Animal pointer, it stops barking and starts making generic animal noises.

This chapter explains why that happens (Static Binding) and how to fix it using **Virtual Functions** (Dynamic Binding), unlocking the true potential of C++.

Section 1: The Trap of Static Binding

Let's revisit our animal hierarchy. We have a base class Animal and a derived class Dog.


class Animal {
public:
    void makeSound() {
        std::cout << "Generic Animal Sound" << std::endl;
    }
};

class Dog : public Animal {
public:
    void makeSound() {
        std::cout << "Woof! Woof!" << std::endl;
    }
};

Now, look at what happens when we use pointers to manage these objects. This scenario is common because, in professional software, we often keep lists of different objects (like a list of generic Animal*) and want to treat them all uniformly.


void the_problem() {
    Animal* myPet = new Dog(); // A pointer of type Animal*, pointing to a Dog object

    // PROBLEM: This calls Animal::makeSound(), NOT Dog::makeSound()!
    myPet->makeSound(); 
    
    // Output: Generic Animal Sound
}

Why did this happen?

This is called **Static Binding** (or Early Binding). When the C++ compiler sees the line myPet->makeSound(), it looks at the type of the pointer myPet. Since myPet is defined as an Animal*, the compiler hard-codes a jump to the Animal version of the function. It ignores the fact that the object *actually* sitting in memory is a Dog. The decision is made at compile-time, and it cannot be changed.

Section 2: The Solution—Virtual Functions

To fix this, we need to tell the compiler: "Don't decide which function to call right now. Wait until the program is running, look at the object, and call the correct function for that specific object."

We do this using the keyword **virtual**.


class Animal {
public:
    // The 'virtual' keyword enables Dynamic Binding
    virtual void makeSound() {
        std::cout << "Generic Animal Sound" << std::endl;
    }
};

class Dog : public Animal {
public:
    // The 'override' keyword (C++11) ensures we are actually overriding a virtual function
    void makeSound() override {
        std::cout << "Woof! Woof!" << std::endl;
    }
};

void the_solution() {
    Animal* myPet = new Dog(); 

    // SUCCESS: This now looks at the object type (Dog) at runtime
    myPet->makeSound(); 
    
    // Output: Woof! Woof!
}

Dynamic Binding and the V-Table

When a function is marked virtual, C++ uses **Dynamic Binding** (Late Binding). The compiler adds a small hidden pointer to the class (called the vptr) which points to a lookup table called the **V-Table** (Virtual Table).

At runtime, when you call myPet->makeSound(), the program follows the vptr to the table, finds the correct address for the Dog version of the function, and executes it. This adds a tiny bit of overhead (nanoseconds), but it grants us incredible flexibility.

Best Practice: The override Keyword
Introduced in C++11, you should always add override to the function signature in the derived class. This tells the compiler to check your work. If you make a typo (e.g., makeSounds() instead of makeSound()), the compiler will give you an error, saving you from hours of debugging.

Section 3: Polymorphism in Action

The true power of polymorphism is realized when you have a collection of different objects that share a common base class. You can loop through them and command them all to perform an action, and each one will perform it differently based on its specific type.


#include <vector>

class Cat : public Animal {
public:
    void makeSound() override {
        std::cout << "Meow!" << std::endl;
    }
};

void zoo_simulation() {
    // A vector of pointers to the Base Class
    std::vector<Animal*> zoo;

    zoo.push_back(new Dog());
    zoo.push_back(new Cat());
    zoo.push_back(new Dog());
    zoo.push_back(new Animal());

    std::cout << "The zoo is waking up:" << std::endl;

    // Iterate through the zoo
    for (Animal* animal : zoo) {
        // Polymorphism: The same line of code produces different results!
        animal->makeSound();
    }

    // Cleanup memory (we will discuss virtual destructors next)
    for (Animal* animal : zoo) {
        delete animal;
    }
}

Output:

The zoo is waking up:
Woof! Woof!
Meow!
Woof! Woof!
Generic Animal Sound

Section 4: Abstract Classes and Pure Virtual Functions

Sometimes, a base class is just a concept—a blueprint that shouldn't actually exist on its own. For example, what does a generic "Shape" look like? You can draw a Circle, and you can draw a Square, but you cannot draw just a "Shape."

In C++, we represent this using **Pure Virtual Functions**. A pure virtual function has no implementation in the base class. It is essentially a contract that says: "Any class inheriting from me *must* implement this function."

A class containing at least one pure virtual function is called an **Abstract Class**. You **cannot** create an instance of an abstract class.


// Abstract Base Class
class Shape {
public:
    // Syntax for Pure Virtual Function (= 0)
    virtual void draw() = 0; 
    
    // Normal virtual function
    virtual double getArea() { return 0.0; }
};

class Circle : public Shape {
public:
    void draw() override {
        std::cout << "Drawing a Circle" << std::endl;
    }
};

class Square : public Shape {
public:
    void draw() override {
        std::cout << "Drawing a Square" << std::endl;
    }
};

void shape_demo() {
    // Shape s; // ERROR! Cannot instantiate abstract class
    
    Shape* s1 = new Circle();
    Shape* s2 = new Square();
    
    s1->draw(); // Output: Drawing a Circle
    s2->draw(); // Output: Drawing a Square
}

Section 5: The Virtual Destructor Rule

There is one final, critical rule regarding polymorphism. If you plan to allow a class to be inherited from and used polymorphically (accessed via a base pointer), you **MUST** make the base class destructor virtual.

The Danger

If the base destructor is not virtual, when you call delete basePtr;, only the base part of the object is cleaned up. The derived part (which might be holding memory, open files, etc.) is left untouched. This causes a memory leak.


class Base {
public:
    // CORRECT: Virtual Destructor
    virtual ~Base() { 
        std::cout << "Base Destroyed" << std::endl; 
    }
};

class Derived : public Base {
private:
    int* data;
public:
    Derived() { data = new int[100]; } // Allocates memory
    ~Derived() { 
        delete[] data; // Cleans up memory
        std::cout << "Derived Destroyed" << std::endl; 
    }
};

void correct_cleanup() {
    Base* ptr = new Derived();
    
    // Because ~Base() is virtual, it calls ~Derived() first, then ~Base()
    delete ptr; 
}

Chapter 16 Conclusion

You have now unlocked the full capability of Object-Oriented Programming.

  • **Polymorphism** allows you to write code that works on general types (like Shape or Animal) while effectively handling specific implementations (Circle or Dog) at runtime.
  • **Abstract Classes** allow you to define strict interfaces (contracts) that enforce design rules on your derived classes.
  • **Virtual Destructors** ensure that memory is managed safely across these complex hierarchies.

With OOP mastered, we shift our focus back to practical utilities. A program isn't very useful if it can't save its data.

In **Chapter 17**, we will explore **File Input/Output (I/O)**. You will learn how to read text files, write logs, and persist your object data to the hard drive using streams.