Polymorphism in C++ is an object-oriented programming concept that allows objects to be treated as instances of their parent class rather than their actual class.
It enables a single interface to represent different underlying data types, allowing flexibility and extensibility in code.
There are two types of polymorphism in C++:
- Compile-time Polymorphism (Static): Achieved using function overloading and operator overloading.
- Runtime Polymorphism (Dynamic): Achieved using inheritance and virtual functions, where the function to execute is determined at runtime based on the object type.
Benefits of Polymorphism
- Flexibility: Allows implementing a single interface for various types.
- Code Reusability: Reduces code duplication by allowing common interfaces.
- Scalability: Simplifies adding new functionality by extending the base class.
Let’s explore polymorphism in C++ with examples.
1. Compile-time Polymorphism: Function Overloading
Function overloading allows multiple functions with the same name but different parameters.
The compiler decides which function to call based on the parameter list.
#include <iostream> using namespace std; class Printer { public: void print(int value) { cout << "Integer: " << value << endl; } void print(double value) { cout << "Double: " << value << endl; } void print(const string& value) { cout << "String: " << value << endl; } }; int main() { Printer printer; printer.print(10); // Calls print(int) printer.print(10.5); // Calls print(double) printer.print("Hello World"); // Calls print(string) return 0; }
Explanation:
- The print function is overloaded with different parameter types: int, double, and string.
- The compiler selects the appropriate print function based on the argument type.
Output:
Integer: 10 Double: 10.5 String: Hello World
2. Compile-time Polymorphism: Operator Overloading
Operator overloading allows defining custom behaviors for operators when applied to user-defined types.
#include <iostream> using namespace std; class Complex { private: double real, imag; public: Complex(double r, double i) : real(r), imag(i) {} // Overloading the + operator Complex operator + (const Complex& other) { return Complex(real + other.real, imag + other.imag); } void display() const { cout << real << " + " << imag << "i" << endl; } }; int main() { Complex c1(2.5, 3.5); Complex c2(1.5, 2.5); Complex result = c1 + c2; // Uses overloaded + operator result.display(); return 0; }
Explanation:
- The + operator is overloaded to add two Complex numbers.
- c1 + c2 uses the overloaded + operator to produce a new Complex object.
Output:
4 + 6i
3. Runtime Polymorphism: Virtual Functions
In runtime polymorphism, the function call is determined at runtime based on the type of the object, achieved through inheritance and virtual functions.
#include <iostream> using namespace std; class Animal { public: virtual void sound() { // Virtual function cout << "Some generic animal sound" << endl; } }; class Dog : public Animal { public: void sound() override { // Override base class function cout << "Woof Woof" << endl; } }; class Cat : public Animal { public: void sound() override { // Override base class function cout << "Meow Meow" << endl; } }; int main() { Animal* animal1 = new Dog(); Animal* animal2 = new Cat(); animal1->sound(); // Calls Dog's sound() animal2->sound(); // Calls Cat's sound() delete animal1; delete animal2; return 0; }
Explanation:
- Animal has a virtual function sound.
- Dog and Cat override sound with their specific implementations.
- Calling sound through Animal* pointers invokes the appropriate function based on the actual object type (Dog or Cat).
Output:
Woof Woof Meow Meow
4. Pure Virtual Functions and Abstract Classes
A pure virtual function has no implementation in the base class and makes the class abstract, meaning it cannot be instantiated. Derived classes must implement the pure virtual function.
#include <iostream> using namespace std; class Shape { public: virtual void draw() = 0; // Pure virtual function }; class Circle : public Shape { public: void draw() override { cout << "Drawing Circle" << endl; } }; class Square : public Shape { public: void draw() override { cout << "Drawing Square" << endl; } }; int main() { Shape* shape1 = new Circle(); Shape* shape2 = new Square(); shape1->draw(); shape2->draw(); delete shape1; delete shape2; return 0; }
Explanation:
- Shape is an abstract class due to the pure virtual function draw.
- Circle and Square classes implement draw.
- The program calls draw using Shape* pointers, which invokes the derived class’s implementation.
Output:
Drawing Circle Drawing Square
5. Virtual Destructor for Proper Cleanup in Polymorphism
When using polymorphism, it’s essential to have a virtual destructor in the base class to ensure proper cleanup.
#include <iostream> using namespace std; class Base { public: Base() { cout << "Base Constructor" << endl; } virtual ~Base() { // Virtual destructor cout << "Base Destructor" << endl; } }; class Derived : public Base { public: Derived() { cout << "Derived Constructor" << endl; } ~Derived() { cout << "Derived Destructor" << endl; } }; int main() { Base* basePtr = new Derived(); delete basePtr; // Calls both Derived and Base destructors return 0; }
Explanation:
- Base has a virtual destructor, ensuring that Derived’s destructor is called when deleting a Base* pointer.
- Without a virtual destructor, only Base’s destructor would be called, causing a potential memory leak if Derived allocated dynamic memory.
Output:
Base Constructor Derived Constructor Derived Destructor Base Destructor
6. Virtual Functions in Real-World Example: Payment System
In a payment system, different payment methods like CreditCard and PayPal can be managed through a common interface using virtual functions.
#include <iostream> using namespace std; class PaymentMethod { public: virtual void processPayment(double amount) = 0; // Pure virtual function }; class CreditCard : public PaymentMethod { public: void processPayment(double amount) override { cout << "Processing Credit Card payment of $" << amount << endl; } }; class PayPal : public PaymentMethod { public: void processPayment(double amount) override { cout << "Processing PayPal payment of $" << amount << endl; } }; int main() { PaymentMethod* payment1 = new CreditCard(); PaymentMethod* payment2 = new PayPal(); payment1->processPayment(100.0); payment2->processPayment(200.0); delete payment1; delete payment2; return 0; }
Explanation:
- PaymentMethod is an abstract class with a pure virtual function processPayment.
- CreditCard and PayPal classes implement processPayment with specific logic.
- By using PaymentMethod* pointers, different payment methods can be processed through the same interface.
Output:
Processing Credit Card payment of $100 Processing PayPal payment of $200
7. Function Overriding in Polymorphism
When a derived class has a function with the same name as the base class, it overrides the base function.
If the base function is virtual, the derived class function is called, demonstrating runtime polymorphism.
#include <iostream> using namespace std; class Animal { public: virtual void makeSound() const { cout << "Animal sound" << endl; } }; class Dog : public Animal { public: void makeSound() const override { cout << "Bark" << endl; } }; class Cat : public Animal { public: void makeSound() const override { cout << "Meow" << endl; } }; void playSound(const Animal& animal) { animal.makeSound(); } int main() { Dog dog; Cat cat; playSound(dog); // Calls Dog's makeSound() playSound(cat); // Calls Cat's makeSound() return 0; }
Explanation:
- makeSound is a virtual function in Animal, overridden in Dog and Cat.
- playSound accepts an Animal reference, allowing it to call makeSound polymorphically.
Output:
Bark Meow
Summary Table of Polymorphism Concepts
Polymorphism Concept | Description | Example |
---|---|---|
Function Overloading | Multiple functions with the same name but different parameters | print(int), print(double) |
Operator Overloading | Custom behavior for operators with user-defined types | Overloading + for Complex |
Virtual Function | Allows function overriding and polymorphic behavior | sound() in Animal |
Pure Virtual Function | Creates an abstract class, requiring derived classes to implement | draw() in Shape |
Virtual Destructor | Ensures proper cleanup of derived classes | ~Base() in base class |
Real-world Payment System | Abstract payment processing through a virtual interface | processPayment() in PaymentMethod |
Function Overriding | Derived class provides specific implementation for base class method | makeSound() in Animal |
Complete Example: Polymorphism in an Employee Management System
This example demonstrates polymorphism by defining different employee types with a common interface for calculating salaries.
#include <iostream> using namespace std; class Employee { public: virtual double calculateSalary() const = 0; // Pure virtual function virtual void displayRole() const = 0; }; class Manager : public Employee { public: double calculateSalary() const override { return 8000.0; // Manager salary } void displayRole() const override { cout << "Manager" << endl; } }; class Developer : public Employee { public: double calculateSalary() const override { return 5000.0; // Developer salary } void displayRole() const override { cout << "Developer" << endl; } }; class Intern : public Employee { public: double calculateSalary() const override { return 2000.0; // Intern salary } void displayRole() const override { cout << "Intern" << endl; } }; void printEmployeeDetails(const Employee& emp) { emp.displayRole(); cout << "Salary: $" << emp.calculateSalary() << endl; } int main() { Manager manager; Developer developer; Intern intern; printEmployeeDetails(manager); printEmployeeDetails(developer); printEmployeeDetails(intern); return 0; }
Output:
Manager Salary: $8000 Developer Salary: $5000 Intern Salary: $2000
This example demonstrates polymorphism by allowing different types of employees to implement their calculateSalary and displayRole methods.
The function printEmployeeDetails operates on an Employee reference, calling the correct method based on the actual employee type.
Key Takeaways
Polymorphism in C++ enables flexible and scalable code by allowing objects to interact through a common interface while supporting distinct behaviors in derived classes.
With polymorphism, you can:
- Use function overloading and operator overloading for compile-time polymorphism.
- Implement virtual functions and pure virtual functions for runtime polymorphism.
- Achieve abstraction, extensibility, and improved code organization, especially when dealing with complex systems.