C++ Polymorphism
Polymorphism means “many forms” — it lets code written against a base class work correctly with any derived class, calling the right version of a function automatically based on the actual object type at runtime. It is one of the four pillars of object-oriented programming and is what makes C++ class hierarchies genuinely useful: instead of writing separate code for every subtype, you write one function that operates on a base pointer or reference, and each derived object responds in its own way. Without polymorphism, inheritance is just code reuse; with it, inheritance becomes a way to model behavior that varies by type.
What Polymorphism Is and How It Works
C++ actually has two kinds of polymorphism. Compile-time polymorphism (also called static polymorphism) is resolved by the compiler before the program runs — this includes function overloading and templates, where the compiler picks the correct function based on argument types at compile time. Run-time polymorphism (dynamic polymorphism) is resolved while the program is executing, and it is what most people mean when they say “C++ polymorphism.” It is achieved through virtual functions accessed via a base class pointer or reference.
Here is the key idea: when a base class function is declared virtual, calling that function through a base class pointer or reference does not necessarily call the base class’s version. Instead, C++ looks at the actual, dynamic type of the object being pointed to and calls the most-derived override of that function. This is called dynamic dispatch, and it is fundamentally different from normal (non-virtual) function calls, which are resolved statically based on the declared type of the variable, not the object it happens to point to.
Internally, the compiler implements this using a mechanism called the virtual table (vtable). Every class that declares or inherits virtual functions gets one vtable — essentially an array of function pointers, one per virtual function, pointing at the most-derived implementation. Every object of such a class carries a hidden pointer, the vptr, usually stored as the first bytes of the object, that points to its class’s vtable. When you call a virtual function through a pointer or reference, the compiler generates code that follows the vptr to the vtable and calls whatever function pointer sits at that function’s slot. This indirection is why polymorphism only works through pointers or references — calling a virtual function on an object by value uses the compile-time (static) type, because there is no dynamic type ambiguity when you have the actual object sitting in front of you.
Syntax
class Base {
public:
virtual ReturnType functionName(Parameters) {
// default/base implementation
}
virtual ~Base() {} // virtual destructor
};
class Derived : public Base {
public:
ReturnType functionName(Parameters) override {
// derived-specific implementation
}
};
Base* ptr = new Derived();
ptr->functionName(); // calls Derived's version
| Keyword / Element | Meaning |
|---|---|
virtual |
Marks a member function as overridable; enables dynamic dispatch through base pointers/references. |
override |
Optional but recommended keyword on a derived function; tells the compiler to verify it actually overrides a base virtual function (catches typos). |
= 0 |
Makes a function pure virtual, meaning it has no implementation and the class becomes abstract (cannot be instantiated). |
virtual ~Base() |
A virtual destructor; required whenever a class may be deleted through a base class pointer. |
Base* / Base& |
Pointer or reference to the base class — the mechanism through which dynamic dispatch happens. |
Examples
Example 1: Basic Virtual Function Overriding
#include <iostream>
using namespace std;
class Animal {
public:
virtual void speak() const {
cout << "The animal makes a sound." << endl;
}
virtual ~Animal() {}
};
class Dog : public Animal {
public:
void speak() const override {
cout << "The dog barks." << endl;
}
};
class Cat : public Animal {
public:
void speak() const override {
cout << "The cat meows." << endl;
}
};
int main() {
Animal* animals[3];
Dog d;
Cat c;
Animal a;
animals[0] = &a;
animals[1] = &d;
animals[2] = &c;
for (int i = 0; i < 3; i++) {
animals[i]->speak();
}
return 0;
}
Output:
The animal makes a sound.
The dog barks.
The cat meows.
Even though every element of the array is typed as Animal*, each call to speak() resolves to the override that matches the object it actually points to. This is dynamic dispatch in action: the compiler cannot know at compile time which speak() will run, so it defers the decision to runtime via the vtable.
Example 2: Abstract Base Classes with Pure Virtual Functions
#include <iostream>
using namespace std;
class Shape {
public:
virtual double area() const = 0; // pure virtual
virtual void describe() const {
cout << "This shape has area " << area() << endl;
}
virtual ~Shape() {}
};
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
double area() const override {
return 3.14159 * radius * radius;
}
};
class Rectangle : public Shape {
private:
double width, height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
double area() const override {
return width * height;
}
};
int main() {
Circle circ(4.0);
Rectangle rect(3.0, 5.0);
Shape* shapes[2] = { &circ, &rect };
for (int i = 0; i < 2; i++) {
shapes[i]->describe();
}
return 0;
}
Output:
This shape has area 50.2654
This shape has area 15
Shape declares area() as pure virtual (= 0), which means Shape itself cannot be instantiated — you can never write Shape s;. It exists purely as an interface that Circle and Rectangle must implement. Notice that describe() is a normal virtual function with a body that calls the pure virtual area(); this is a common and powerful pattern where a base class defines shared behavior that depends on a piece each derived class must supply.
Example 3: Virtual Destructors and Cleanup Order
#include <iostream>
using namespace std;
class Base {
public:
Base() { cout << "Base constructed" << endl; }
virtual ~Base() { cout << "Base destroyed" << endl; }
};
class Derived : public Base {
private:
int* data;
public:
Derived() {
data = new int[10];
cout << "Derived constructed" << endl;
}
~Derived() override {
delete[] data;
cout << "Derived destroyed" << endl;
}
};
int main() {
Base* basePtr = new Derived();
delete basePtr;
return 0;
}
Output:
Base constructed
Derived constructed
Derived destroyed
Base destroyed
Because ~Base() is virtual, delete basePtr correctly calls ~Derived() first (freeing data), then chains up to ~Base(). If the destructor were not virtual, only ~Base() would run, leaking the array and leaving Derived‘s resources uncleaned — this is one of the most common sources of subtle bugs in C++ inheritance.
Under the Hood: The Vtable Mechanism
When a class contains at least one virtual function, the compiler generates one vtable per class (shared by all instances) and adds a hidden vptr field to every object of that class, typically as the first machine word. Constructing an object sets its vptr to point at its own class’s vtable — and this happens layer by layer during construction, which is exactly why calling a virtual function from inside a base class constructor will not dispatch to a derived override: at that point the vptr still points at the base class’s vtable because the derived part of the object has not been constructed yet.
A virtual call like ptr->speak() compiles to roughly: follow ptr‘s vptr to the vtable, index into the slot reserved for speak, and call whatever function address is stored there. This indirection costs a small, constant amount of extra work compared to a normal function call (a couple of pointer dereferences), and it also adds one pointer’s worth of size to every polymorphic object. For the vast majority of programs this overhead is negligible, but it explains why C++ does not make every function virtual by default — you pay for dynamic dispatch only when you ask for it.
Common Mistakes
Mistake 1: Forgetting the virtual Keyword
If the base class function is not declared virtual, calling it through a base pointer always uses the base class’s version, no matter what the actual object is — this is called static (early) binding.
#include <iostream>
using namespace std;
class Animal {
public:
void speak() const { cout << "Animal sound" << endl; } // not virtual
};
class Dog : public Animal {
public:
void speak() const { cout << "Woof" << endl; }
};
int main() {
Animal* a = new Dog();
a->speak(); // prints "Animal sound", NOT "Woof"
delete a;
return 0;
}
Output:
Animal sound
This compiles cleanly, which makes it dangerous — there is no error, just wrong behavior. The fix is simply to mark the base function virtual:
#include <iostream>
using namespace std;
class Animal {
public:
virtual void speak() const { cout << "Animal sound" << endl; }
};
class Dog : public Animal {
public:
void speak() const override { cout << "Woof" << endl; }
};
int main() {
Animal* a = new Dog();
a->speak(); // now correctly prints "Woof"
delete a;
return 0;
}
Output:
Woof
Mistake 2: Object Slicing
Passing a derived object by value to a function expecting the base class type strips away the derived part entirely — this is called slicing, and it happens even if the function is virtual, because a new Base object is copy-constructed from the Derived one.
#include <iostream>
using namespace std;
class Animal {
public:
virtual void speak() const { cout << "Animal sound" << endl; }
};
class Dog : public Animal {
public:
void speak() const override { cout << "Woof" << endl; }
};
void announce(Animal a) { // passed BY VALUE -> slicing
a.speak();
}
int main() {
Dog d;
announce(d); // Dog part is sliced off
return 0;
}
Output:
Animal sound
The fix is to pass by pointer or reference so no copy is made and the vptr of the real object is preserved:
#include <iostream>
using namespace std;
class Animal {
public:
virtual void speak() const { cout << "Animal sound" << endl; }
};
class Dog : public Animal {
public:
void speak() const override { cout << "Woof" << endl; }
};
void announce(const Animal& a) { // reference -> no slicing
a.speak();
}
int main() {
Dog d;
announce(d);
return 0;
}
Output:
Woof
Best Practices
- Always give a base class a
virtualdestructor if it will ever be deleted through a base pointer, even if the destructor body is empty. - Use the
overridekeyword on every derived override — the compiler will flag mismatched signatures instead of silently creating a new, unrelated function. - Pass and return polymorphic objects by pointer or reference, never by value, to avoid object slicing.
- Prefer pure virtual functions to define interfaces/abstract base classes rather than giving every method a dummy default body.
- Never call a virtual function from a constructor or destructor expecting derived behavior — the vptr is not fully set up yet at those points.
- Mark classes or specific overrides
finalwhen you deliberately want to stop further overriding. - Keep class hierarchies shallow; deep hierarchies make dynamic dispatch harder to reason about and often signal a design that should favor composition instead.
Practice Exercises
- Exercise 1: Create a base class
Employeewith a virtual functiondouble calculatePay(). DeriveSalariedEmployeeandHourlyEmployeeclasses that override it differently, then compute total payroll by looping over an array ofEmployee*. - Exercise 2: Write an abstract class
Vehiclewith a pure virtual functionstartEngine()and a non-pure virtual functiondescribe()that calls it. ImplementCarandMotorcyclesubclasses and verifyVehiclecannot be instantiated directly. - Exercise 3: Take the slicing example from this lesson and modify
announceto accept anAnimalby value on purpose. Add astd::string namemember toDogand observe what happens to it after slicing — explain why in a comment.
Summary
- Polymorphism lets base class pointers/references call the correct derived-class function automatically at runtime.
- Declaring a function
virtualenables dynamic dispatch; without it, calls are resolved statically using the pointer’s declared type. - Each polymorphic class gets a vtable, and each object holds a hidden vptr pointing at its class’s vtable — this is how the runtime lookup works.
- Pure virtual functions (
= 0) make a class abstract and define an interface that derived classes must implement. - A base class intended for polymorphic use needs a
virtualdestructor to avoid resource leaks and undefined behavior. - Object slicing occurs when a derived object is copied into a base object by value, discarding the derived data — always use pointers or references.
- Use
overrideeverywhere to let the compiler catch mismatched overrides at compile time.
