Polymorphism in C++
Polymorphism is one of the four pillars of Object-Oriented Programming, alongside encapsulation, inheritance, and abstraction. The word comes from the Greek roots meaning "many forms." In C++, polymorphism lets you write a single interface that works across multiple types — a capability that separates junior developers from engineers who crack FAANG interviews at ₹30–50 LPA packages.
Every time you see a company like Infosys or TCS building a banking system where a single processPayment() call handles credit cards, UPI, and net banking uniformly, polymorphism is at work underneath.
Two Flavours of Polymorphism
C++ gives you polymorphism in two distinct flavours, resolved at different points in the compilation and execution pipeline.
| Type | Also Called | Resolved At | Mechanism |
|---|---|---|---|
| Compile-time | Static polymorphism | Compile time | Function overloading, operator overloading, templates |
| Runtime | Dynamic polymorphism | Run time | Virtual functions, vtable |
Compile-Time Polymorphism
Function Overloading
The compiler selects the correct function version by examining the number and types of arguments you pass. No runtime cost is involved.
#include <iostream>
using namespace std;
// Three versions of the same name — compiler picks based on arguments
double area(double radius) {
return 3.14159 * radius * radius;
}
double area(double length, double breadth) {
return length * breadth;
}
double area(double a, double b, double c) {
// Heron's formula for triangle
double s = (a + b + c) / 2.0;
return sqrt(s * (s - a) * (s - b) * (s - c));
}
int main() {
cout << "Circle area: " << area(5.0) << endl;
cout << "Rectangle area: " << area(4.0, 6.0) << endl;
cout << "Triangle area: " << area(3.0, 4.0, 5.0) << endl;
return 0;
}
The compiler sees area(5.0) and matches it to the single-parameter version. This decision happens entirely at compile time — zero overhead at runtime.
A Note on Operator Overloading
Operator overloading is also compile-time polymorphism. Because it is a large topic, the next chapter covers it in depth. For now, know that +, ==, and << can all be given custom meanings for your classes.
Runtime Polymorphism
Runtime polymorphism is more powerful and more frequently tested in FAANG technical screens. It allows you to call a method on a base-class pointer and have the correct derived-class version execute — even when the base class has no idea which derived class will be used.
The Problem Without Virtual Functions
#include <iostream>
using namespace std;
class Animal {
public:
void speak() {
cout << "Some generic animal sound" << endl;
}
};
class Dog : public Animal {
public:
void speak() {
cout << "Woof!" << endl;
}
};
int main() {
Animal* ptr = new Dog();
ptr->speak(); // Prints: "Some generic animal sound" — NOT "Woof!"
delete ptr;
return 0;
}
The pointer is of type Animal*, so the compiler binds speak() to Animal::speak() at compile time. The Dog version is never called. This is called early binding or static dispatch.
Introducing the virtual Keyword
Adding virtual to the base class method instructs the compiler to use late binding — the actual function to call is determined at runtime based on the object's true type.
#include <iostream>
using namespace std;
class Animal {
public:
virtual void speak() {
cout << "Some generic animal sound" << endl;
}
};
class Dog : public Animal {
public:
void speak() override { // 'override' is good practice (C++11)
cout << "Woof!" << endl;
}
};
class Cat : public Animal {
public:
void speak() 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]->speak(); // Correct version called for each
}
delete animals[0];
delete animals[1];
return 0;
}
Output:
Woof!
Meow!
The vtable — How It Works Under the Hood
Understanding the vtable (virtual function table) is mandatory knowledge for senior engineering interviews at companies like Google India or Flipkart.
When a class has at least one virtual function, the compiler creates a vtable for that class — a static array of function pointers, one per virtual method. Every object of that class contains a hidden pointer (the vptr) that points to its class's vtable.
Animal vtable:
[0] --> Animal::speak
Dog vtable:
[0] --> Dog::speak
Cat vtable:
[0] --> Cat::speak
When you call ptr->speak(), the CPU:
- Follows
ptrto the object in memory. - Reads the object's vptr to find the vtable.
- Looks up index 0 in the vtable to find the function address.
- Calls that function.
This is one extra pointer dereference compared to a non-virtual call — the runtime overhead is negligible in practice but theoretically measurable in extremely tight loops.
Pure Virtual Functions and Abstract Classes
Sometimes a base class concept is so general that implementing the function makes no sense. What is the "area" of an arbitrary Shape? The answer depends on whether it is a circle, rectangle, or triangle.
C++ lets you declare a function as pure virtual by assigning it = 0. A class with at least one pure virtual function becomes an abstract class — you cannot instantiate it directly.
#include <iostream>
#include <cmath>
using namespace std;
class Shape {
public:
// Pure virtual — no implementation in Shape
virtual double area() const = 0;
virtual double perimeter() const = 0;
// Non-virtual utility usable by all shapes
void printInfo() const {
cout << "Area: " << area() << endl;
cout << "Perimeter: " << perimeter() << endl;
}
virtual ~Shape() {} // Always provide a virtual destructor
};
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
double area() const override {
return 3.14159265 * radius * radius;
}
double perimeter() const override {
return 2.0 * 3.14159265 * radius;
}
};
class Rectangle : public Shape {
private:
double length, breadth;
public:
Rectangle(double l, double b) : length(l), breadth(b) {}
double area() const override {
return length * breadth;
}
double perimeter() const override {
return 2.0 * (length + breadth);
}
};
int main() {
// Shape s; // ERROR — cannot instantiate abstract class
Shape* shapes[2];
shapes[0] = new Circle(7.0);
shapes[1] = new Rectangle(5.0, 3.0);
for (int i = 0; i < 2; i++) {
shapes[i]->printInfo();
cout << "---" << endl;
}
delete shapes[0];
delete shapes[1];
return 0;
}
Output:
Area: 153.938
Perimeter: 43.9823
---
Area: 15
Perimeter: 16
---
The override Keyword (C++11)
Before C++11, you could accidentally misspell a virtual function name in a derived class and the compiler would silently create a new, unrelated function instead of overriding the base.
class Shape {
public:
virtual double area() const = 0;
};
class Circle : public Shape {
public:
// Without 'override' — compiles fine even if signature mismatches
double Area() const { return 0; } // Oops — capital A, never overrides
};
With override, the compiler checks that you are genuinely overriding a base class virtual function:
class Circle : public Shape {
public:
double Area() const override { return 0; }
// ERROR: 'Area' does not override any base class virtual function
};
Always use override — it catches typos and signature mismatches at compile time.
The final Keyword (C++11)
final prevents further overriding or inheritance. Use it when a design decision is intentional and you want the compiler to enforce it.
class Circle : public Shape {
public:
double area() const override final {
return 3.14159 * radius * radius;
}
};
class SpecialCircle : public Circle {
public:
double area() const override { ... } // COMPILE ERROR — area() is final
};
You can also mark an entire class as final:
class Singleton final {
// No class can inherit from Singleton
};
Virtual Destructors — Why They Matter
This is a classic bug that shows up in TCS NQT technical rounds and FAANG coding rounds alike.
class Base {
public:
~Base() { cout << "Base destroyed" << endl; }
};
class Derived : public Base {
public:
int* data;
Derived() { data = new int[100]; }
~Derived() {
delete[] data;
cout << "Derived destroyed" << endl;
}
};
int main() {
Base* ptr = new Derived();
delete ptr; // Only Base::~Base() is called — memory leak!
return 0;
}
Because the destructor is not virtual, delete ptr only invokes Base::~Base(). The Derived destructor never runs, so data leaks.
The fix is always to declare the base class destructor virtual:
class Base {
public:
virtual ~Base() { cout << "Base destroyed" << endl; }
};
Now delete ptr correctly calls Derived::~Derived() first, then Base::~Base().
Rule of thumb: If a class has any virtual function, its destructor must also be virtual.
Worked Example: Shape Hierarchy
Let us build a complete, production-quality shape hierarchy that could appear in an interview problem or a geometry module for a competitive exam platform like GATE preparation software.
#include <iostream>
#include <vector>
#include <cmath>
#include <string>
using namespace std;
// Abstract base — cannot be instantiated
class Shape {
public:
virtual double area() const = 0;
virtual double perimeter() const = 0;
virtual string name() const = 0;
void describe() const {
cout << name()
<< " | Area: " << area()
<< " | Perimeter: " << perimeter()
<< endl;
}
virtual ~Shape() {}
};
class Circle final : public Shape {
double r;
public:
explicit Circle(double radius) : r(radius) {}
double area() const override { return M_PI * r * r; }
double perimeter() const override { return 2.0 * M_PI * r; }
string name() const override { return "Circle(r=" + to_string(r) + ")"; }
};
class Rectangle : public Shape {
double w, h;
public:
Rectangle(double width, double height) : w(width), h(height) {}
double area() const override { return w * h; }
double perimeter() const override { return 2.0 * (w + h); }
string name() const override {
return "Rectangle(" + to_string(w) + "x" + to_string(h) + ")";
}
};
class Triangle : public Shape {
double a, b, c;
public:
Triangle(double x, double y, double z) : a(x), b(y), c(z) {}
double area() const override {
double s = (a + b + c) / 2.0;
return sqrt(s * (s - a) * (s - b) * (s - c));
}
double perimeter() const override { return a + b + c; }
string name() const override { return "Triangle"; }
};
// Polymorphic function — works for ANY Shape subclass
double totalArea(const vector<Shape*>& shapes) {
double sum = 0;
for (const Shape* s : shapes) {
sum += s->area();
}
return sum;
}
int main() {
vector<Shape*> shapes;
shapes.push_back(new Circle(5.0));
shapes.push_back(new Rectangle(4.0, 6.0));
shapes.push_back(new Triangle(3.0, 4.0, 5.0));
for (const Shape* s : shapes) {
s->describe();
}
cout << "\nTotal area of all shapes: " << totalArea(shapes) << endl;
for (Shape* s : shapes) delete s;
return 0;
}
Output:
Circle(r=5.000000) | Area: 78.5398 | Perimeter: 31.4159
Rectangle(4.000000x6.000000) | Area: 24 | Perimeter: 20
Triangle | Area: 6 | Perimeter: 12
Total area of all shapes: 108.54
The totalArea function never needs to change regardless of how many new shape types you add — this is the Open/Closed Principle in action.
Common Pitfalls
-
Forgetting the virtual destructor. Deleting a derived object through a base pointer leaks resources when the destructor is not virtual. Make it virtual whenever inheritance is involved.
-
Calling virtual functions from constructors or destructors. During construction of a
Base, the vtable points toBase's methods, not the derived class. Virtual dispatch does not work the way you expect inside constructors. -
Slicing. Assigning a derived object to a base-class value (not pointer or reference) copies only the base portion. Always use pointers or references for polymorphism.
-
Missing
overridekeyword. A derived method with a slightly different signature silently creates a new function rather than overriding. Useoverrideeverywhere. -
Pure virtual confusion. A class with even one pure virtual function is abstract. Forgetting to implement all pure virtuals in the derived class makes the derived class abstract too, which surprises newcomers.
Practice Exercises
-
Create an abstract class
Vehiclewith pure virtual methodsfuelType()andmaxSpeed(). DeriveElectricCar,PetrolBike, andDieselTruckfrom it. Print all vehicle details using a base-class pointer array. -
Extend the
Shapehierarchy with aSquareclass that inherits fromRectangle. Useoverrideandfinalappropriately. Ensure the virtual destructor chain is correct. -
Write a function
largestShape(vector<Shape*>)that returns the shape with the greatest area. Use polymorphism — no type-checking allowed. -
Demonstrate the virtual-destructor bug with a class that allocates memory in its constructor. Fix it and confirm with console output that both destructors run.
-
Build a small polymorphic
Employeehierarchy (Manager,Developer,Intern) where each class overridescalculateBonus(). Compute the total bonus payout across a team stored in a base-class pointer vector — a scenario common in Infosys HR system design problems.
Summary
- Polymorphism means "many forms" — one interface, multiple implementations.
- Compile-time polymorphism (function overloading, templates) is resolved at compile time with no runtime overhead.
- Runtime polymorphism uses
virtualfunctions; the correct version is selected at runtime via the vtable. - The vtable is a per-class array of function pointers; every object with virtual methods carries a hidden vptr.
- A pure virtual function (
= 0) makes a class abstract — it cannot be instantiated. - Use
overrideto catch signature mismatches at compile time. - Use
finalto prevent further overriding or inheritance. - Always declare the base destructor
virtualwhen using inheritance with pointers, or you will leak memory. - Polymorphism enables the Open/Closed Principle: add new types without changing existing code.