🧱 Section 3 · OOP 🟡 Intermediate MODULE 13

Polymorphism & Virtual Functions

⏱️ 25 min
📖 Theory + Code
🧩 5 Quiz Questions
🏗️ 1 Challenge
Your progress in Section 375%
🎯 What you'll learn: What polymorphism actually means, a real C++ gotcha — why calling a method through a base class pointer runs the base version by default — the virtual keyword that fixes it, the override keyword for safety, and a first look at pure virtual functions and abstract classes.

What Is Polymorphism?

Polymorphism ("many forms") means you can treat objects of different derived classes through a single common type — usually a base class pointer or reference — and have the correct, specific behaviour run automatically for whatever object actually sits behind that pointer.

Using the Animal/Dog/Cat hierarchy from Lesson 12: imagine you have a list of Animal* pointers, some pointing to Dog objects, some to Cat objects. Polymorphism lets you loop through that list calling animal->speak() on each one — and get "Woof!" for the dogs and "Meow!" for the cats, without writing any if checks for the type.

🎭
One interface, many behaviours
Polymorphism is what lets a game engine call update() on a list of totally different game objects, or a UI framework call draw() on a list of different shapes — the caller doesn't need to know or care exactly which derived type it's dealing with.

The Gotcha — Why Plain Functions Don't Work

Here's the part that surprises almost every C++ beginner: by default, member functions are not polymorphic. If you call a method through a base class pointer, C++ runs the base class's version of that method — even if the pointer is actually pointing at a derived object.

without_virtual.cpp — THE GOTCHA
C++
#include <iostream>
using namespace std;

class Animal {
public:
    // NOT virtual — this is the mistake
    void speak() {
        cout << "The animal makes a sound." << endl;
    }
};

class Dog : public Animal {
public:
    void speak() {
        cout << "The dog barks." << endl;
    }
};

int main() {
    Dog d;
    Animal* ptr = &d;   // a base pointer to a derived object

    ptr->speak();       // you'd EXPECT "The dog barks." — but you get the base version!

    return 0;
}

Output — probably not what you expected:

Console Output
OUTPUT
The animal makes a sound.
⚠️
Why this happens: static binding
By default, C++ decides at compile time which version of speak() to call, based on the declared type of the pointer (Animal*) — not the actual object it points to. This is called static (compile-time) binding. To get the version matching the real object, you need dynamic (run-time) binding — which requires the virtual keyword.

The virtual Keyword — Fixing the Gotcha

Mark the base class method virtual, and C++ switches to dynamic dispatch: at run time, it looks at the actual object behind the pointer and calls that object's version of the function — exactly the behaviour polymorphism promises.

with_virtual.cpp — FIXED
C++
#include <iostream>
using namespace std;

class Animal {
public:
    virtual void speak() {   // now virtual — enables dynamic dispatch
        cout << "The animal makes a sound." << endl;
    }
};

class Dog : public Animal {
public:
    void speak() override {   // override — this replaces Animal's version
        cout << "The dog barks." << endl;
    }
};

int main() {
    Dog d;
    Animal* ptr = &d;

    ptr->speak();   // NOW correctly calls Dog::speak()

    return 0;
}

Output — now correct:

Console Output
OUTPUT
The dog barks.

The only change was adding virtual to the base class declaration. Once a function is virtual in the base class, it stays virtual through the entire hierarchy — you don't need to repeat the keyword in every derived override (though many style guides recommend it for readability).

The override Keyword — Safety Net

Writing override after a derived function's parameter list (as in void speak() override) tells the compiler: "I intend for this to override a virtual function from my base class." If you make a typo — wrong name, wrong parameter types, or forgot virtual in the base — the compiler will now give you an error instead of silently compiling broken code.

Always add override when you mean to override
Without override, a typo like void Speak() (capital S) just quietly creates a brand-new, unrelated function instead of overriding — and you'd get the Section 2 gotcha again, with no warning. override turns that silent bug into a compile error.

A Preview: Pure Virtual Functions & Abstract Classes

Sometimes a base class shouldn't provide any default implementation — it should just force every derived class to supply its own. You do that with a pure virtual function, written with = 0 instead of a body:

abstract_preview.cpp
C++
class Shape {
public:
    virtual double area() const = 0;   // pure virtual — no body, makes Shape abstract
};

class Circle : public Shape {
private:
    double radius;

public:
    Circle(double r) : radius(r) {}

    double area() const override {
        return 3.14159 * radius * radius;
    }
};

// Shape s;         ← COMPILE ERROR: cannot instantiate an abstract class
// Circle c(2.0);   ← fine: Circle implements area(), so it can be created

A class containing at least one pure virtual function becomes an abstract class — you cannot create objects of it directly. It exists purely to define a required interface ("every Shape must be able to report its area") that concrete derived classes like Circle must implement. You'll use this pattern often once you start designing larger C++ programs.

Lesson Summary

Polymorphism lets you call the correct derived behaviour through a common base pointer/reference.
Without virtual, C++ uses static binding — calling through a base pointer always runs the base class's version, regardless of the real object.
Marking the base method virtual switches to dynamic dispatch — the correct derived version runs at run time.
override on a derived function is a safety net — it turns override typos into compile errors.
A pure virtual function (= 0) has no body and makes its class abstract — it cannot be instantiated directly.
🧩 Knowledge Check — Lesson 13
Answer all 5 questions to test your understanding. Instant feedback on every answer.
1. What does polymorphism let you do?
2. Without the virtual keyword, calling a method through a base class pointer to a derived object runs:
3. Which keyword enables correct dynamic dispatch to derived class overrides?
4. What is the main benefit of writing override on a derived function?
5. What is a pure virtual function (written with = 0)?
💪
Coding Challenge — Lesson 13
Apply what you learned · Intermediate Level

Time to prove polymorphism works. Complete the challenge below in any C++ compiler or online IDE.

Challenge: Make Animal, Dog & Cat truly polymorphic 🎭

Take the Animal/Dog/Cat hierarchy from Lesson 12 and:

1. Add a virtual void speak() method to Animal that prints a generic sound.
2. Override speak() in Dog (prints "Woof!") and in Cat (prints "Meow!"), using override.
3. In main(), create an array (or a few separate variables) of Animal* pointers, some pointing at Dog objects, some at Cat objects.
4. Loop through them calling speak() on each pointer, and confirm each animal makes the correct sound — not the generic base sound.
💡 Show hints if you're stuck
  • Base declaration: virtual void speak() { cout << name << " makes a sound." << endl; }
  • Try: Animal* animals[2]; Dog d("Rex",3); Cat c("Tom",2); animals[0] = &d; animals[1] = &c;
  • Loop with: for (int i = 0; i < 2; i++) animals[i]->speak();
  • If you forget virtual on the base, you'll see the Section 2 gotcha reappear — try removing it to see the difference!
Finished this lesson?
Mark it complete to track your progress.
🎉

Lesson 13 Complete!

You now understand classes, inheritance, and polymorphism — the three pillars you need for real OOP design. Time to combine them all in a capstone project.

Module 13 of 26 Section 3 — Object-Oriented Programming