C++ tutorials  /  Polymorphism in C++
Chapter 13 · C++

Polymorphism in C++

Polymorphism, meaning 'many forms', is an OOP principle that allows the same function name, operator, or interface to behave differently depending on the context in which it's used or the actual type of object it operates on. C++ supports two main kinds of polymorphism: compile-time (static) polymorphism, resolved by the compiler before the program runs (via function/operator overloading), and runtime (dynamic) polymorphism, resolved while the program is executing (via virtual functions and inheritance).

Think of the word 'drive' — you 'drive' a car, you 'drive' a golf ball, and you 'drive' a point home in an argument, and the exact same word means something completely different depending on the context. That's polymorphism: one name, many behaviors, depending on the situation. In C++, if you have a base class 'Shape' with a method 'draw()', and derived classes 'Circle' and 'Square' each override 'draw()' with their own version, you can write code that calls 'draw()' on a generic Shape pointer, and — like magic — the CORRECT version (Circle's or Square's) automatically runs depending on what the object actually is underneath, even though your code only ever mentions the generic 'Shape' type.

A media player app has a base 'MediaFile' class with a 'play()' method, and derived classes 'AudioFile', 'VideoFile', and 'PodcastFile' each override play() with completely different logic (decode audio vs. render video frames vs. stream a podcast). The app's playlist can hold a mixed list of MediaFile pointers and simply call 'file->play()' on each one — thanks to runtime polymorphism, the correct specific behavior automatically executes for each file type without the playlist code needing to know or check what type each file actually is. Similarly, a drawing app like Figma has a base 'Shape' class, and calling 'shape->draw()' on a list containing Rectangles, Circles, and Lines automatically invokes each shape's own specific drawing logic — this is exactly how such graphics editors render mixed collections of different shape types through one unified interface.

Without polymorphism, code that needs to work with multiple related but distinct types (like different Shape subclasses) would require explicit type-checking logic everywhere (e.g., long if-else or switch chains checking 'if this is a Circle, do X; if this is a Square, do Y'), which becomes unwieldy and requires modifying that checking logic every time a new type is added. Polymorphism lets code be written generically against a common base interface, automatically adapting its behavior based on the actual object type at runtime, which makes systems dramatically more extensible (new derived types can be added without modifying existing code that uses the base interface) and is foundational to how large-scale, flexible software architectures — from GUI frameworks to game engines — are designed.

  • Compile-Time Polymorphism (Function Overloading): Achieved by defining multiple functions with the same name but different parameter lists within the same scope; the compiler determines which version to call at compile time based on the number, order, or types of arguments provided in the call.
  • Compile-Time Polymorphism (Operator Overloading): Allows standard C++ operators (like '+', '==', '<<') to be redefined for user-defined class types, enabling intuitive, natural syntax when working with custom objects (e.g., adding two 'Complex' number objects using 'c1 + c2' instead of a named method).
  • Runtime Polymorphism (Virtual Functions): Achieved by declaring a base class method as 'virtual' and overriding it in derived classes; when called through a base class pointer or reference, the CORRECT derived class version executes based on the object's actual runtime type, not the pointer/reference's declared type.
  • Pure Virtual Functions and Abstract Classes: A pure virtual function (declared with '= 0') has no implementation in the base class and MUST be overridden by any derived class; a class containing at least one pure virtual function becomes an 'abstract class' that cannot be instantiated directly, serving purely as an interface/contract that derived classes must fulfill.
// Function overloading returnType functionName(int a); returnType functionName(double a); // Operator overloading class ClassName { public: ClassName operator+(const ClassName &other) { // custom addition logic } }; // Virtual function (runtime polymorphism) class Base { public: virtual void show() { // default behavior } }; class Derived : public Base { public: void show() override { // overridden behavior } }; // Pure virtual function (abstract class) class AbstractBase { public: virtual void mustImplement() = 0; // no implementation here };
A developer building a drawing application has a base 'Shape' class with a 'draw()' method, and derived classes 'Circle' and 'Rectangle' that each need to render themselves differently — the developer needs runtime polymorphism so that calling 'draw()' through a generic Shape pointer automatically invokes each shape's own specific drawing logic, and also wants to overload the '+' operator for a 'Point' class to allow intuitive addition syntax.
Runtime Polymorphism with Virtual Functions
Demonstrates a base class pointer calling a virtual function, with the correct derived class version automatically executing based on the actual object type at runtime.
cpp
#include <iostream> using namespace std; class Shape { public: virtual void draw() { cout << "Drawing a generic shape" << endl; } }; class Circle : public Shape { public: void draw() override { cout << "Drawing a circle" << endl; } }; class Rectangle : public Shape { public: void draw() override { cout << "Drawing a rectangle" << endl; } }; int main() { Shape* shapes[3]; shapes[0] = new Circle(); shapes[1] = new Rectangle(); shapes[2] = new Shape(); for (int i = 0; i < 3; i++) { shapes[i]->draw(); } for (int i = 0; i < 3; i++) delete shapes[i]; return 0; }
Drawing a circle Drawing a rectangle Drawing a generic shape
Even though every element in the 'shapes' array is declared as a 'Shape*' pointer, calling 'draw()' on each one invokes the CORRECT version based on what the object actually IS at runtime (Circle, Rectangle, or plain Shape) — this only works because 'draw()' is marked 'virtual' in the base class, enabling dynamic dispatch. Without 'virtual', all three calls would incorrectly print "Drawing a generic shape", since the compiler would resolve the call based on the pointer's declared type instead.
Operator Overloading for Custom Object Addition
Shows how to overload the '+' operator so two custom 'Point' objects can be added together using natural, intuitive syntax.
cpp
#include <iostream> using namespace std; class Point { public: int x, y; Point(int x, int y) : x(x), y(y) {} Point operator+(const Point &other) { return Point(x + other.x, y + other.y); } void display() { cout << "(" << x << ", " << y << ")" << endl; } }; int main() { Point p1(2, 3); Point p2(4, 5); Point p3 = p1 + p2; p3.display(); return 0; }
(6, 8)
The '+' operator is overloaded as a member function of 'Point', defining exactly what 'p1 + p2' should mean for this class — adding the corresponding x and y components and returning a new 'Point'. This lets 'Point' objects be added using the same natural '+' syntax as built-in numeric types, making the code more readable than an equivalent named method like 'p1.add(p2)'.
  • Forgetting the virtual Keyword, Causing Static Binding Instead of Dynamic Binding: If a base class method is NOT marked 'virtual', calling it through a base class pointer/reference always executes the BASE class's version, regardless of the object's actual derived type — this is a common and confusing bug for beginners expecting polymorphic behavior, since the compiler resolves the call statically at compile time based on the pointer's declared type instead of the object's real type.
  • Slicing When Assigning a Derived Object to a Base Class Object (Not a Pointer/Reference): Assigning a derived class object directly to a base class object BY VALUE (e.g., 'Shape s = circleObj;', not using a pointer or reference) causes 'object slicing' — only the base class portion is copied, and any derived-specific data/behavior is silently 'sliced off' and lost. Polymorphism only works correctly through pointers or references to the base class, never through plain base class objects holding derived data by value.
  • Trying to Instantiate an Abstract Class Directly: A class containing at least one pure virtual function (declared with '= 0') becomes abstract and cannot be instantiated directly (e.g., 'AbstractBase obj;' causes a compile error) — it can only be used as a base class, with a derived class providing concrete implementations for all its pure virtual functions before THAT derived class can be instantiated.
  • Forgetting a Virtual Destructor in a Base Class Used Polymorphically: If a base class is meant to be used polymorphically (via base class pointers to derived objects) and objects are deleted through a base class pointer (e.g., 'delete basePtr;' where basePtr actually points to a Derived object), the base class's destructor MUST be marked 'virtual' — otherwise, only the base class's destructor runs, and the derived class's destructor (along with any cleanup it performs) is skipped entirely, causing resource leaks.
  • Confusing Function Overloading with Function Overriding: Overloading means defining multiple functions with the SAME name but DIFFERENT parameter lists within the same class (resolved at compile time). Overriding means a derived class providing its own implementation of a virtual function with the EXACT SAME signature as the base class version (resolved at runtime). Beginners often use these terms interchangeably, but they represent fundamentally different mechanisms — compile-time versus runtime polymorphism.
  • Always Mark Base Class Destructors as virtual When the Class Is Used Polymorphically: If a class has ANY virtual function and is intended to be used through base class pointers, always declare its destructor as 'virtual' too (e.g., 'virtual ~Shape() {}'), ensuring that deleting a derived object through a base class pointer correctly calls the entire derived-to-base destructor chain, preventing resource leaks.
  • Use the override Keyword Explicitly When Overriding Virtual Functions: In C++11 and later, add 'override' after a derived class's function signature (e.g., 'void draw() override {}') when intending to override a base class virtual function — this lets the compiler verify the signature genuinely matches an existing virtual function in the base class, catching typos or signature mismatches at compile time rather than silently creating an unrelated new function.
  • Use Pure Virtual Functions to Define Clear Interfaces/Contracts: When designing a base class meant purely to define a common interface that all derived classes must implement (with no sensible default behavior of its own), use pure virtual functions ('= 0') to make this an abstract class, enforcing at compile time that every concrete derived class provides its own implementation.
  • Always Work with Base Class Pointers or References, Never Plain Base Class Objects, to Achieve Polymorphism: To get correct polymorphic behavior, store and pass objects through base class POINTERS or REFERENCES (e.g., 'Shape* ptr' or 'Shape& ref'), never as plain base class objects by value, since assigning a derived object to a plain base class object causes object slicing and permanently loses the derived-specific behavior and data.
What is the difference between compile-time (static) polymorphism and runtime (dynamic) polymorphism in C++?
Compile-time polymorphism is resolved by the compiler before the program executes, achieved through function overloading (multiple functions with the same name but different parameters) and operator overloading, where the compiler determines the exact function to call based on the arguments' types and count at compile time. Runtime polymorphism is resolved while the program is actually executing, achieved through virtual functions accessed via base class pointers or references, where the specific derived class implementation to invoke is determined dynamically based on the object's actual type at runtime, enabling flexible, extensible code that works generically across a whole family of related types.
What is a virtual function, and how does it enable runtime polymorphism?
A virtual function is a member function declared in a base class using the 'virtual' keyword, which signals to the compiler that derived classes may provide their own overriding implementation, and that calls to this function through a base class pointer or reference should be resolved dynamically at runtime based on the object's actual type, rather than statically at compile time based on the pointer/reference's declared type. This is implemented internally via a 'vtable' (virtual table) — a hidden table of function pointers maintained per class — that the program consults at runtime to determine which specific override to actually execute.
What is a pure virtual function, and what is an abstract class?
A pure virtual function is a virtual function declared in a base class with no implementation, denoted by setting it equal to zero (e.g., 'virtual void draw() = 0;'), which forces every non-abstract derived class to provide its own concrete implementation. A class containing at least one pure virtual function automatically becomes an 'abstract class', meaning it cannot be instantiated directly — it can only serve as a base class defining a required interface/contract that concrete derived classes must fulfill before they themselves can be instantiated.
What is object slicing in C++, and how can it be avoided?
Object slicing occurs when a derived class object is assigned to a base class object BY VALUE (not through a pointer or reference), causing only the base class portion of the object to be copied — any additional data members or overridden behavior specific to the derived class are silently 'sliced off' and permanently lost, and the resulting object behaves purely as a base class object from that point forward. It's avoided by always working with base class pointers or references (e.g., 'Shape*' or 'Shape&') when polymorphic behavior is needed, rather than plain base class objects holding derived data directly by value.
Why is it important to declare a base class's destructor as virtual when the class will be used polymorphically?
If a base class's destructor is NOT virtual, and a derived class object is deleted through a base class pointer (e.g., 'Shape* s = new Circle(); delete s;'), only the base class's destructor executes — the derived class's destructor (and any cleanup logic or resource deallocation it performs, like freeing dynamically allocated memory specific to the derived class) is skipped entirely, causing memory leaks or resource leaks. Marking the base class destructor 'virtual' ensures that deleting through a base class pointer correctly triggers the full destructor chain, starting with the derived class's destructor and then proceeding up to the base class's destructor.
Create an abstract base class 'Animal' with a pure virtual function 'makeSound()', then derive 'Dog' and 'Cat' classes that each implement it, and demonstrate calling makeSound() polymorphically through a Animal pointer array.
#include <iostream> using namespace std; class Animal { public: virtual void makeSound() = 0; virtual ~Animal() {} }; class Dog : public Animal { public: void makeSound() override { cout << "Woof!" << endl; } }; class Cat : public Animal { public: void makeSound() override { cout << "Meow!" << endl; } }; int main() { Animal* animals[2]; animals[0] = new Dog(); animals[1] = new Cat(); for (int i = 0; i < 2; i++) { animals[i]->makeSound(); } for (int i = 0; i < 2; i++) delete animals[i]; return 0; }
Overload the '==' operator for a 'Fraction' class (with numerator and denominator) to compare if two fractions are mathematically equal.
#include <iostream> using namespace std; class Fraction { public: int numerator, denominator; Fraction(int n, int d) : numerator(n), denominator(d) {} bool operator==(const Fraction &other) { return (numerator * other.denominator) == (other.numerator * denominator); } }; int main() { Fraction f1(1, 2); Fraction f2(2, 4); if (f1 == f2) { cout << "Fractions are equal" << endl; } else { cout << "Fractions are not equal" << endl; } return 0; }
Predict the output and explain why: class Base { public: void show() { cout << "Base show\n"; } }; class Derived : public Base { public: void show() { cout << "Derived show\n"; } }; int main() { Base* ptr = new Derived(); ptr->show(); return 0; }
The output is 'Base show', NOT 'Derived show'. This is because 'show()' is NOT declared 'virtual' in the base class, so the compiler resolves the call STATICALLY at compile time based on the pointer's DECLARED type ('Base*'), completely ignoring the object's actual runtime type (Derived). To get 'Derived show' printed instead (true polymorphic behavior), 'show()' would need to be declared 'virtual' in the 'Base' class.

Polymorphism allows the same function name or operator to exhibit different behavior depending on context, split into compile-time polymorphism (function/operator overloading, resolved by the compiler before execution) and runtime polymorphism (virtual functions, resolved dynamically based on an object's actual type). Understanding virtual functions, pure virtual functions and abstract classes, the critical importance of virtual destructors, and pitfalls like object slicing are essential for building flexible, extensible C++ systems where new derived types can be added without modifying existing code that operates on a common base interface. Polymorphism, combined with encapsulation and inheritance, completes the core toolkit needed to design robust, real-world object-oriented C++ applications.

© 2026 CompileX. Maintained by Aditya Kumar Sharma.