Polymorphism, meaning "many forms," is one of the most powerful features of object-oriented programming. It allows objects of different classes to be treated uniformly through a common interface, enabling code to work with objects without knowing their specific types. This flexibility makes programs more extensible and maintainable, as you can add new types without modifying existing code that uses the base interface.

Introduction to Polymorphism

Polymorphism means "many forms". In C++, it allows objects of different classes to be treated as objects of a common base class. There are two types: compile-time (static) and runtime (dynamic) polymorphism. Compile-time polymorphism is resolved during compilation through function overloading and operator overloading, while runtime polymorphism is resolved during program execution through virtual functions.

Runtime polymorphism is particularly powerful because it enables you to write code that works with base class pointers or references, but calls the appropriate derived class functions based on the actual object type. This is essential for building flexible, extensible systems where new types can be added without modifying existing code.

Understanding polymorphism is crucial for mastering C++ and object-oriented design. It enables design patterns, makes code more maintainable, and allows you to build systems that can evolve and adapt to new requirements without major restructuring.

Types of Polymorphism

1. Compile-time Polymorphism

Also known as static polymorphism. Resolved at compile time through function overloading and operator overloading:

Function Overloading

#include <iostream>
using namespace std;

class Calculator {
public:
    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() {
    Calculator calc;
    cout << calc.add(5, 3) << endl;        // int version
    cout << calc.add(5.5, 3.2) << endl;    // double version
    cout << calc.add(1, 2, 3) << endl;     // three parameters
    return 0;
}

Operator Overloading

#include <iostream>
using namespace std;

class Complex {
private:
    int real, imag;

public:
    Complex(int r = 0, int i = 0) {
        real = r;
        imag = i;
    }
    
    Complex operator + (const Complex &obj) {
        Complex res;
        res.real = real + obj.real;
        res.imag = imag + obj.imag;
        return res;
    }
    
    void display() {
        cout << real << " + " << imag << "i" << endl;
    }
};

int main() {
    Complex c1(3, 4);
    Complex c2(1, 2);
    Complex c3 = c1 + c2;  // Operator overloading
    c3.display();
    return 0;
}

2. Runtime Polymorphism

Also known as dynamic polymorphism. Resolved at runtime through virtual functions:

Virtual Functions

Virtual functions allow runtime polymorphism. They are declared with the virtual keyword:

#include <iostream>
using namespace std;

class Base {
public:
    virtual void display() {  // Virtual function
        cout << "Base class display" << endl;
    }
};

class Derived : public Base {
public:
    void display() {  // Overrides base class function
        cout << "Derived class display" << endl;
    }
};

int main() {
    Base *ptr;
    Derived d;
    ptr = &d;
    
    ptr->display();  // Calls Derived::display() due to virtual
    
    return 0;
}

Pure Virtual Functions

A virtual function with no implementation. Makes the class abstract:

#include <iostream>
using namespace std;

class Shape {
public:
    virtual void draw() = 0;  // Pure virtual function
    virtual double area() = 0;  // Pure virtual function
};

class Circle : public Shape {
private:
    double radius;

public:
    Circle(double r) : radius(r) {}
    
    void draw() {
        cout << "Drawing circle" << endl;
    }
    
    double area() {
        return 3.14159 * radius * radius;
    }
};

class Rectangle : public Shape {
private:
    double length, width;

public:
    Rectangle(double l, double w) : length(l), width(w) {}
    
    void draw() {
        cout << "Drawing rectangle" << endl;
    }
    
    double area() {
        return length * width;
    }
};

int main() {
    Shape *s1 = new Circle(5.0);
    Shape *s2 = new Rectangle(4.0, 6.0);
    
    s1->draw();
    cout << "Area: " << s1->area() << endl;
    
    s2->draw();
    cout << "Area: " << s2->area() << endl;
    
    delete s1;
    delete s2;
    return 0;
}

Abstract Classes

A class with at least one pure virtual function is called an abstract class. Objects of abstract classes cannot be created:

class AbstractClass {
public:
    virtual void pureFunction() = 0;  // Makes class abstract
    void concreteFunction() {
        cout << "This is a concrete function" << endl;
    }
};

// Cannot create: AbstractClass obj;  // Error!
// Must derive and implement pure virtual function

Virtual Destructor

When deleting objects through base class pointers, use virtual destructor to ensure proper cleanup:

#include <iostream>
using namespace std;

class Base {
public:
    virtual ~Base() {  // Virtual destructor
        cout << "Base destructor" << endl;
    }
};

class Derived : public Base {
public:
    ~Derived() {
        cout << "Derived destructor" << endl;
    }
};

int main() {
    Base *ptr = new Derived();
    delete ptr;  // Calls both destructors due to virtual
    return 0;
}

Function Overriding vs Overloading

FeatureOverloadingOverriding
ScopeSame classDifferent classes (inheritance)
SignatureMust differMust be same
ResolutionCompile timeRuntime (with virtual)
KeywordNot requiredvirtual (for runtime)

Summary

TopicKey PointsDifficulty
Compile-time PolymorphismFunction/operator overloading, resolved at compile time, no runtime overheadBeginner
Runtime PolymorphismVirtual functions, resolved at runtime, enables dynamic dispatchIntermediate
Virtual FunctionsEnable runtime polymorphism, use virtual keyword, can be overriddenIntermediate
Pure Virtual FunctionsNo implementation (= 0), makes class abstract, must be implemented in derivedIntermediate
Abstract ClassesCannot be instantiated, contains pure virtual functions, defines interfaceIntermediate
Virtual DestructorEnsures proper cleanup when deleting through base pointer, prevents memory leaksIntermediate
Overriding vs OverloadingOverriding: same signature, different classes. Overloading: different signatures, same classIntermediate

Frequently Asked Questions

Q1: What is the difference between compile-time and runtime polymorphism?

Compile-time polymorphism (function/operator overloading) is resolved by the compiler based on function signatures. Runtime polymorphism (virtual functions) is resolved at runtime based on the actual object type. Compile-time has no runtime overhead, while runtime polymorphism uses virtual function tables (vtables) for dynamic dispatch.

Q2: When should I use virtual functions?

Use virtual functions when you need runtime polymorphism - when you have base class pointers/references pointing to derived objects and want to call the derived class version. Virtual functions enable dynamic dispatch, allowing the correct function to be called based on the actual object type, not the pointer type.

Q3: What happens if I don't use virtual keyword?

Without virtual, function calls are resolved based on the pointer/reference type, not the actual object type. This means a base class pointer to a derived object will call the base class function, not the derived one. This is called static binding. Virtual enables dynamic binding.

Q4: What is a pure virtual function and why use it?

A pure virtual function has no implementation and is declared with = 0. It makes the class abstract (cannot be instantiated) and forces derived classes to provide implementations. Use pure virtual functions to define interfaces - contracts that derived classes must fulfill. This enforces a consistent interface across different implementations.

Q5: Why do I need a virtual destructor?

When deleting an object through a base class pointer, without a virtual destructor, only the base destructor is called, leading to incomplete cleanup and potential memory leaks. Virtual destructor ensures the derived destructor is called first, then the base destructor, ensuring proper cleanup of all resources.

Q6: Can I have a virtual constructor in C++?

No, constructors cannot be virtual in C++. Constructors are called to create objects, and at construction time, the object type is already known. However, you can use virtual clone() or factory methods to achieve similar functionality. Destructors can and should be virtual when using inheritance with polymorphism.

Q7: What is the performance cost of virtual functions?

Virtual functions have a small performance overhead: one extra indirection through the vtable (virtual function table) and inability to inline in some cases. However, this overhead is usually negligible compared to the flexibility gained. Modern compilers optimize virtual function calls, and the benefits of polymorphism typically outweigh the small performance cost.

Q8: Can I override a non-virtual function?

You can define a function with the same name in a derived class, but it's hiding, not overriding. Without virtual, the function called depends on the pointer/reference type, not the object type. Use the overridekeyword (C++11+) to ensure you're actually overriding a virtual function and catch errors at compile time.

Conclusion

Polymorphism is a cornerstone of object-oriented programming that enables flexible, extensible code. Compile-time polymorphism through overloading provides convenience and type safety, while runtime polymorphism through virtual functions enables powerful dynamic behavior that adapts to the actual object types.

Virtual functions and abstract classes enable you to define interfaces and create systems where new types can be added without modifying existing code. This is essential for building maintainable, scalable software. The virtual destructor ensures proper cleanup in polymorphic hierarchies, preventing resource leaks.

Mastery of polymorphism, combined with inheritance and encapsulation, enables you to build sophisticated software systems using design patterns and best practices. Understanding when to use compile-time vs runtime polymorphism, and how to design effective class hierarchies, is crucial for becoming a proficient C++ programmer.

Related Links

🔹 Author: Dr. J. Siva Ramakrishna

🔹 Institution: Narayana Engineering College, Gudur

🔹 Last Updated: 9 January 2026