Paramclasses Academy - Blog

Tutorials Details

What is Polymorphism in C++

What is Polymorphism in C++

October 16, 2026

Welcome to Module 13 of Param Classes. In Module 12, you learned inheritance. Now you’ll learn how polymorphism lets different classes respond to the same function call in different ways.

Polymorphism is one of the core concepts of object-oriented programming (OOP). It helps you design flexible programs in which common interfaces can represent different behaviors.

Learning objectives

By the end of this module, you will be able to:

  • Explain compile-time and runtime polymorphism.

  • Use function overloading and operator overloading.

  • Override virtual functions in derived classes.

  • Understand base-class pointers and dynamic dispatch.

  • Create abstract classes using pure virtual functions.

  • Apply polymorphism in a practical C++ program.

    Prerequisites

    Functions, classes, inheritance, constructors

    Estimated time

    60–90 minutes

1. What is polymorphism?

The word polymorphism means “many forms.”

In C++, polymorphism allows the same function name, interface, or operation to have different behavior depending on the context or the actual object.

Imagine a media player:

  • An audio file plays sound.

  • A video file plays both visuals and sound.

  • A podcast may support audio playback with additional controls.

A common operation such as play() can represent different implementations.

Media

Common interface: play()

Audio

Plays sound

Video

Plays visuals

Podcast

Plays episodesOne conceptual interface, multiple possible implementations.

There are two major forms of polymorphism in C++:

Type

When behavior is selected

Common mechanisms

Compile-time polymorphism

During compilation

Function overloading, operator overloading, templates

Runtime polymorphism

During program execution

Virtual functions and overriding through base pointers or references

We’ll start with compile-time polymorphism, then move to runtime polymorphism and abstract classes.

2. Function overloading

Function overloading means defining multiple functions with the same name but different parameter lists.

The compiler chooses the appropriate overload based on the arguments used in the call.

Syntax

returnType functionName(type1 parameter);
returnType functionName(type1 parameter, type2 parameter);

The parameter lists must differ in a way that permits overload resolution. You cannot overload functions solely by changing their return types.

Example: overloaded add()


#include 
using namespace std;

int add(int a, int b) {
    return a + b;
}

double add(double a, double b) {
    return a + b;
}

int add(int a, int b, int c) {
    return a + b + c;
}

int main() {
    cout << add(4, 6) << '\n';
    cout << add(2.5, 3.5) << '\n';
    cout << add(1, 2, 3) << '\n';

    return 0;
}

Output

10
6
6

Explanation

  • add(4, 6) selects the overload with two int parameters.

  • add(2.5, 3.5) selects the overload with two double parameters.

  • add(1, 2, 3) selects the overload with three int parameters.

The function name stays the same, but the parameter list determines which overload is selected.

Overloading rules to remember

These can distinguish overloads:

  • Number of parameters.

  • Parameter types.

  • Parameter order.

These alone cannot distinguish overloads:

  • Return type.

  • Parameter names.

For example, these two declarations are not valid overloads of one another:

int calculate(int x);
double calculate(int x); // Error: differs only by return type

3. Operator overloading

C++ lets you define how certain operators behave for your own class types. This is called operator overloading.

For example, you can define what + means when adding two objects that represent complex numbers.

Example: adding two complex numbers


#include 
using namespace std;

class Complex {
public:
    double real;
    double imag;

    Complex(double r, double i) : real(r), imag(i) {}

    Complex operator+(const Complex& other) const {
        return Complex(real + other.real,
                       imag + other.imag);
    }

    void display() const {
        cout << real << " + " << imag << "i\n";
    }
};

int main() {
    Complex c1(2, 3);
    Complex c2(4, 5);

    Complex result = c1 + c2;
    result.display();

    return 0;
}

Output

6 + 8i

How it works

The expression:

Complex result = c1 + c2;

uses the overloaded operator+ function. It adds the real components and imaginary components separately.

  • Real part: \(2 + 4 = 6\)

  • Imaginary part: \(3 + 5 = 8\)

Important restrictions: operator overloading cannot change an operator's precedence, associativity, or number of operands. At least one operand must be a user-defined class or enumeration type, and some operators cannot be overloaded.

4. Runtime polymorphism with virtual functions

Runtime polymorphism lets C++ select a function implementation based on the actual object being referred to at runtime.

It typically combines three elements:

  1. A base class.

  2. A virtual member function in that base class.

  3. A derived class that overrides the function.

Example: animal sounds


#include 
using namespace std;

class Animal {
public:
    virtual void sound() {
        cout << "Generic animal sound\n";
    }

    virtual ~Animal() = default;
};

class Dog : public Animal {
public:
    void sound() override {
        cout << "Dog barks\n";
    }
};

class Cat : public Animal {
public:
    void sound() override {
        cout << "Cat meows\n";
    }
};

int main() {
    Dog dog;
    Cat cat;

    Animal* first = &dog;
    Animal* second = &cat;

    first->sound();
    second->sound();

    return 0;
}

Output

Dog barks
Cat meows

Why does this work?

The pointers first and second both have the type Animal*. However, they refer to different derived objects.

Because sound() is declared virtual, C++ dispatches each call to the final overrider for the actual object:

  • first->sound() calls Dog::sound().

  • second->sound() calls Cat::sound().

The arrow operator (->) accesses a member through a pointer.

What if virtual is removed?

If sound() is not virtual in Animal, then calling it through an Animal* invokes Animal::sound() in this example. The derived functions may still exist, but the calls through those base pointers do not use virtual dispatch.

This is why virtual is essential to this form of runtime polymorphism.

5. The override keyword

When a derived class intends to override a virtual function, write override explicitly.

class Animal {
public:
    virtual void sound() {}
};

class Dog : public Animal {
public:
    void sound() override {}
};

This tells the compiler to verify that Dog::sound() actually overrides a compatible virtual function from the base class.

For example, accidentally writing void sounds() instead of void sound() with override causes a compilation error rather than silently leaving the intended function unoverridden.

Best practice: use override for derived-class overrides. It makes the intent explicit and catches common mistakes.

6. Abstract classes and pure virtual functions

An abstract class is a class that cannot be instantiated directly. In C++, a class is abstract if it has at least one pure virtual function that has not been overridden to provide a final implementation in that class.

A pure virtual function is declared by appending = 0 to its declaration.

Syntax

class Shape {
public:
    virtual double area() const = 0;
};

The Shape class defines a required interface: concrete derived classes must implement area() before they can be instantiated.

Example: calculating areas


#include 
using namespace std;

class Shape {
public:
    virtual double area() const = 0;

    virtual ~Shape() = default;
};

class Rectangle : public Shape {
private:
    double width;
    double height;

public:
    Rectangle(double w, double h)
        : width(w), height(h) {}

    double area() const override {
        return width * height;
    }
};

class Circle : public Shape {
private:
    double radius;

public:
    Circle(double r) : radius(r) {}

    double area() const override {
        return 3.14159 * radius * radius;
    }
};

int main() {
    Rectangle rectangle(4, 5);
    Circle circle(2);

    Shape* first = &rectangle;
    Shape* second = &circle;

    cout << "Rectangle area: " << first->area() << '\n';
    cout << "Circle area: " << second->area() << '\n';

    return 0;
}

Output

Rectangle area: 20
Circle area: 12.5664

Key points

  • You cannot create an ordinary object of Shape because it is abstract.

  • You can declare a pointer or reference to Shape.

  • Rectangle and Circle implement the area() interface.

  • Calling area() through a Shape* invokes the implementation for the actual derived object.

Abstract classes are useful when several different types share a required interface but need different implementations.

7. Polymorphism using references

Pointers are not the only way to use runtime polymorphism. References work too.


#include 
using namespace std;

class Animal {
public:
    virtual void sound() const {
        cout << "Animal sound\n";
    }

    virtual ~Animal() = default;
};

class Cow : public Animal {
public:
    void sound() const override {
        cout << "Moo\n";
    }
};

void makeSound(const Animal& animal) {
    animal.sound();
}

int main() {
    Cow cow;
    makeSound(cow);

    return 0;
}

Output

Moo

The function makeSound() accepts a reference to the base class. Passing a Cow object binds that reference to the object's base-class portion, while virtual dispatch still selects Cow::sound().

This design avoids requiring a separate function overload for every animal type.

8. Virtual destructors and safe cleanup

If a derived object may be destroyed through a base-class pointer, the base class should generally have a virtual destructor.

class Base {
public:
    virtual ~Base() = default;
};

Why? Consider:

Base* ptr = new Derived();
delete ptr;

A virtual destructor ensures that destruction proceeds through the derived class and then the base class. Without a virtual base destructor, deleting a derived object through a base pointer is undefined behavior.

In modern C++, prefer smart pointers such as std::unique_ptr for dynamic ownership where appropriate. A virtual destructor is still important when polymorphic base-class ownership is intended.

9. Compile-time vs runtime polymorphism: comparison

Feature

Compile-time

Runtime

Common examples

Function and operator overloading

Virtual functions

Selection

During compilation

At runtime through virtual dispatch

Typical setup

Different parameter lists or types

Base interface and derived overrides

Main benefit

One name for related operations

One interface for different implementations

Templates are also a compile-time mechanism in C++. They let code work with multiple types, though they are not the same mechanism as function overloading.

10. Common mistakes

Mistake 1: Expecting runtime dispatch without virtual

A same-named derived function alone does not enable virtual dispatch through a base pointer or reference.

Mistake 2: Overloading by return type alone

Two functions with identical parameter lists cannot be overloaded just because one returns int and the other returns double.

Mistake 3: Trying to instantiate an abstract class

A class with a pure virtual function and no final implementation for it cannot be instantiated directly.

Mistake 4: Forgetting override

The override specifier lets the compiler catch mistakes in the intended overriding signature.

11. Mini-project: Payment Processing System

Let's apply runtime polymorphism to a realistic scenario. Different payment methods share one interface, but calculate their fees differently.

Requirements

  • Create an abstract class Payment.

  • Declare a pure virtual function process().

  • Implement CardPayment and UPIPayment.

  • Call both implementations through base-class pointers.


#include 
#include 
using namespace std;

class Payment {
public:
    virtual void process(double amount) const = 0;

    virtual ~Payment() = default;
};

class CardPayment : public Payment {
public:
    void process(double amount) const override {
        double fee = amount * 0.02;

        cout << "Card payment: " << amount << '\n';
        cout << "Processing fee: " << fee << '\n';
        cout << "Total: " << amount + fee << '\n';
    }
};

class UPIPayment : public Payment {
public:
    void process(double amount) const override {
        cout << "UPI payment: " << amount << '\n';
        cout << "Processing fee: 0\n";
        cout << "Total: " << amount << '\n';
    }
};

int main() {
    CardPayment card;
    UPIPayment upi;

    Payment* methods[] = { &card, &upi };
    double amount = 1000.0;

    for (Payment* method : methods) {
        method->process(amount);
        cout << '\n';
    }

    return 0;
}

Output

Card payment: 1000
Processing fee: 20
Total: 1020

UPI payment: 1000
Processing fee: 0
Total: 1000

The fee rules here are illustrative, not actual claims about payment-provider fees.

Project breakdown

  1. Payment defines the shared interface.

  2. process() is pure virtual, making Payment abstract.

  3. CardPayment and UPIPayment provide different implementations.

  4. Payment* methods[] stores pointers to both concrete objects.

  5. The loop calls the appropriate process() function at runtime.

Extension challenge: Add a WalletPayment class, give it its own processing rule, and add it to the array without changing the loop.

This is one of the main benefits of polymorphism: you can add new implementations without rewriting the code that operates on the common interface.

12. Hands-on exercises

Try the exercises yourself before revealing the solutions. Each solution includes expected output where applicable.

Exercise 1 — Function overloading

Beginner

Exercise 1

Create two functions named display(): one accepts an int and one accepts a string. Call both from main().

Reveal solution Copy

Exercise 2 — Runtime polymorphism

Intermediate

Exercise 2

Create a base class Instrument with a virtual play() function. Derive Guitar and Piano, override play(), and call each function through an Instrument pointer.

Reveal solution Copy

Exercise 3 — Abstract class

Intermediate

Exercise 3

Create an abstract class Appliance with a pure virtual function powerUsage(). Derive Fan, implement powerUsage(), and return 75 watts.

Reveal solution Copy

Exercise 4 — Operator overloading

Intermediate

Exercise 4

Create a class Distance with an integer member meters. Overload + to add two Distance objects and display the result.

Reveal solution Copy

13. Interactive quiz — Test your understanding

Complete these eight questions to check your knowledge of compile-time polymorphism, virtual functions, abstract classes, and operator overloading.

C++ has void print(int) and void print(double). Which mechanism lets the compiler choose between them based on the argument type?

1 of 8

AFunction overloading

BConstructor chaining

CAbstract-class dispatch

DRuntime overriding

Next

14. Module summary

Here are the key concepts from Module 13:

  • Function overloading: same function name, different parameter lists.

  • Operator overloading: define suitable operator behavior for user-defined types.

  • Virtual functions: enable runtime dispatch through base pointers and references.

  • Overriding: provide a derived implementation of an inherited virtual function.

  • override: lets the compiler verify the intended override.

  • Abstract classes: define interfaces that cannot be instantiated directly.

  • Pure virtual functions: declare required behavior using = 0.

  • Virtual destructors: support correct destruction of derived objects through base pointers.

15. Completion checklist

Mark each topic when you feel comfortable explaining it and writing a working example.

Module 13 progress

0/8

I can explain compile-time and runtime polymorphism.

I can implement function overloading with different parameter lists.

I can overload an operator for a custom class.

I can use virtual functions and override them in derived classes.

I understand polymorphism through base-class pointers and references.

I can create an abstract class with a pure virtual function.

I understand why polymorphic base classes often need virtual destructors.

I completed the exercises and reviewed my quiz results.