Chapter 11 of 23

Polymorphism in C++

Master compile-time and runtime polymorphism in C++ — virtual functions, abstract classes, vtables, and the override/final keywords with practical Shape hierarchy examples.

Meritshot12 min read
C++PolymorphismVirtual FunctionsOOPAbstract Classes
All C++ Chapters

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.

TypeAlso CalledResolved AtMechanism
Compile-timeStatic polymorphismCompile timeFunction overloading, operator overloading, templates
RuntimeDynamic polymorphismRun timeVirtual 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:

  1. Follows ptr to the object in memory.
  2. Reads the object's vptr to find the vtable.
  3. Looks up index 0 in the vtable to find the function address.
  4. 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

  1. 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.

  2. Calling virtual functions from constructors or destructors. During construction of a Base, the vtable points to Base's methods, not the derived class. Virtual dispatch does not work the way you expect inside constructors.

  3. 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.

  4. Missing override keyword. A derived method with a slightly different signature silently creates a new function rather than overriding. Use override everywhere.

  5. 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

  1. Create an abstract class Vehicle with pure virtual methods fuelType() and maxSpeed(). Derive ElectricCar, PetrolBike, and DieselTruck from it. Print all vehicle details using a base-class pointer array.

  2. Extend the Shape hierarchy with a Square class that inherits from Rectangle. Use override and final appropriately. Ensure the virtual destructor chain is correct.

  3. Write a function largestShape(vector<Shape*>) that returns the shape with the greatest area. Use polymorphism — no type-checking allowed.

  4. 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.

  5. Build a small polymorphic Employee hierarchy (Manager, Developer, Intern) where each class overrides calculateBonus(). 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 virtual functions; 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 override to catch signature mismatches at compile time.
  • Use final to prevent further overriding or inheritance.
  • Always declare the base destructor virtual when using inheritance with pointers, or you will leak memory.
  • Polymorphism enables the Open/Closed Principle: add new types without changing existing code.