Home C++ Polymorphism in C++ tutorial with code examples

Polymorphism in C++ tutorial with code examples

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++:

  1. Compile-time Polymorphism (Static): Achieved using function overloading and operator overloading.
  2. 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

  1. Flexibility: Allows implementing a single interface for various types.
  2. Code Reusability: Reduces code duplication by allowing common interfaces.
  3. 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.

 

You may also like