Polymorphism is one of the core concepts in object-oriented programming (OOP) that allows objects of different classes to be treated as objects of a common base class. The term polymorphism comes from the Greek words poly (many) and morph (form), meaning many forms.
Polymorphism allows methods to have the same name but behave differently based on the type of the object calling them. It enables the use of a single interface for different underlying forms of data or classes.
There are two main types of polymorphism in C++:
This type of polymorphism is resolved at compile time. The most common examples of compile-time polymorphism are:
In function overloading, multiple functions with the same name can exist in the same scope, but they must differ in the number or type of their parameters. The compiler determines which function to call based on the function signature.
#include <iostream>
using namespace std;
class Printer {
public:
void print(int i) {
cout << "Printing an integer: " << i << endl;
}
void print(double d) {
cout << "Printing a double: " << d << endl;
}
void print(string str) {
cout << "Printing a string: " << str << endl;
}
};
int main() {
Printer printer;
printer.print(10); // Calls print(int)
printer.print(3.14); // Calls print(double)
printer.print("Hello"); // Calls print(string)
return 0;
}
In the example, the print function is overloaded to accept different parameter types (integer, double, and string). The appropriate print function is chosen based on the type of argument passed to it. The decision about which function to call is made at compile-time, hence it is compile-time polymorphism.
In operator overloading, we redefine the meaning of an operator (like +, -, *, etc.) for user-defined types (like classes). This allows us to perform operations on objects of custom classes just as we would on built-in types.
#include <iostream>
using namespace std;
class Complex {
private:
float real;
float imag;
public:
Complex() : real(0), imag(0) {}
Complex(float r, float i) : real(r), imag(i) {}
// Overloading the '+' operator
Complex operator+(const Complex& obj) {
Complex temp;
temp.real = real + obj.real;
temp.imag = imag + obj.imag;
return temp;
}
void display() {
cout << real << " + " << imag << "i" << endl;
}
};
int main() {
Complex c1(3.5, 2.5), c2(1.5, 4.5);
Complex c3 = c1 + c2; // Uses overloaded '+' operator
c3.display(); // Displays the result of c1 + c2
return 0;
}
Here, the + operator is overloaded for the Complex class to add two complex numbers. The operator + now performs addition on the real and imag parts of two Complex objects. Since the operator is overloaded at compile-time, this is an example of compile-time polymorphism.
Runtime polymorphism is resolved at runtime. It allows us to invoke methods of derived classes through a base class pointer or reference. It is achieved through virtual functions and function overriding.
Function overriding occurs when a derived class provides a specific implementation of a method that is already defined in the base class. The function in the base class is marked as virtual to allow dynamic binding at runtime.
#include <iostream>
using namespace std;
class Animal {
public:
// Virtual function
virtual void sound() {
cout << "Animal makes a sound" << endl;
}
};
class Dog : public Animal {
public:
// Overriding the base class function
void sound() override {
cout << "Dog barks" << endl;
}
};
class Cat : public Animal {
public:
// Overriding the base class function
void sound() override {
cout << "Cat meows" << endl;
}
};
int main() {
Animal* animal;
Dog dog;
Cat cat;
// Animal pointer pointing to Dog object
animal = &dog;
animal->sound(); // Output: Dog barks
// Animal pointer pointing to Cat object
animal = &cat;
animal->sound(); // Output: Cat meows
return 0;
}
In this example:
Animal has a virtual function sound(), which is overridden in the derived classes Dog and Cat.main() function, we use a base class pointer (animal) to refer to objects of both derived classes (Dog and Cat).sound() function is called based on the type of object the pointer points to, and this decision is made at runtime, hence it is runtime polymorphism.Key Points:
virtual, enabling dynamic dispatch.sound() method in both Dog and Cat overrides the base class function. At runtime, the appropriate version of sound() is called based on the actual object type (Dog or Cat), not the pointer type.Dynamic binding (also known as late binding) is the process where the function call is resolved at runtime. When a base class pointer or reference points to a derived class object, the function call is bound to the correct method in the derived class.
To ensure proper cleanup of resources when an object is destroyed, virtual destructors are used. If a base class has a virtual function and the object is deleted through a base class pointer, the derived class destructor is called, which ensures proper destruction of the object.
#include <iostream>
using namespace std;
class Base {
public:
// Virtual destructor
virtual ~Base() {
cout << "Base Destructor" << endl;
}
};
class Derived : public Base {
public:
~Derived() override {
cout << "Derived Destructor" << endl;
}
};
int main() {
Base* b = new Derived;
delete b; // Calls the derived class destructor, then the base class destructor
return 0;
}
In this example, a Derived object is created but deleted through a base class pointer. Since the destructor in the base class is virtual, the derived class destructor is called first, followed by the base class destructor, ensuring that the resources are properly freed.
Flexibility and Extensibility: Polymorphism allows you to design systems where new classes can be introduced without changing existing code. For instance, new types of Animal can be added without modifying the code that uses Animal pointers or references.
Code Reusability: Common operations can be defined in base classes, and derived classes can override or extend these operations. This promotes reusability, reducing code duplication.
Simplified Code: Polymorphism simplifies code, especially in cases where similar operations are performed on different object types. It allows for cleaner and more maintainable code by handling different types in a unified manner.
Performance Overhead: There is a performance cost for runtime polymorphism because of the overhead of dynamic dispatch. Each call to a virtual function involves a lookup in the virtual table (vtable), which can be slower than static function calls.
Increased Complexity: Polymorphism, especially dynamic polymorphism, can introduce complexity into the code. It can be harder to track method calls and debug issues when multiple classes with similar interfaces are involved.
Memory Overhead: Virtual functions introduce memory overhead due to the virtual table (vtable), which stores addresses of virtual functions. This increases memory usage, especially in large programs with many polymorphic classes.
Open this section to load past papers