Polymorphism & Virtual Functions
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.
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.
#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:
The animal makes a sound.
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.
#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:
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.
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:
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
virtual, C++ uses static binding — calling through a base pointer always runs the base class's version, regardless of the real object.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.= 0) has no body and makes its class abstract — it cannot be instantiated directly.virtual keyword, calling a method through a base class pointer to a derived object runs:override on a derived function?= 0)?Time to prove polymorphism works. Complete the challenge below in any C++ compiler or online IDE.
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
virtualon the base, you'll see the Section 2 gotcha reappear — try removing it to see the difference!