Polymorphism: Many Forms
Table of Contents
- What is Polymorphism?
- Compile-Time (Static) Polymorphism
- Function Overloading
- Operator Overloading (Complex Numbers)
- Run-Time (Dynamic) Polymorphism
- The Early Binding Problem
- The Virtual Keyword (Late Binding)
- Upcasting vs. Downcasting
- Deep Dive: Virtual Destructors & Memory Leaks
- The
finalKeyword
1. What is Polymorphism?
Polymorphism translates to "many forms". It is the ability of a message, data, or function to be processed in more than one form, or a phenomenon that allows an object to have several different forms and behaviors.
There are two distinct types in C++:
- Compile-Time (Static) Polymorphism
- Run-Time (Dynamic) Polymorphism
2. Compile-Time (Static) Polymorphism
In compile-time polymorphism, the C++ compiler figures out exactly which function to call during compilation. This is called Early Binding.
Function Overloading
Occurs when, in the same class, we have different methods with the same name but with a different number of arguments or different types of arguments.
int sum(int a, int b) { return a + b; }
double sum(double a, double b) { return a + b; } Operator Overloading (The Complex Number Example)
Imagine adding two complex numbers. We need to add the real part to the real part, and the complex part to the complex part. To do this naturally (A + B), we need to override the default functionality of the + operator.
Important Note: In the example below, we don’t take two complex numbers as arguments. When two objects A and B are added like A + B, the compiler treats it like A.operator+(B). It's like passing B into A's function!
#include <iostream>
using namespace std;
class Complex {
public:
int real, complex;
Complex(int _real, int _complex) : real(_real), complex(_complex) {}
// Operator Overloading syntax:
// return_type operator [symbol] (argument) {}
Complex operator + (const Complex &obj) {
// We add 'this->real' (A's real) with 'obj.real' (B's real)
return Complex(this->real + obj.real, this->complex + obj.complex);
}
};
int main() {
Complex A(10, 5);
Complex B(2, 4);
Complex C = A + B; // Calls A.operator+(B)
cout << "Real: " << C.real << ", Complex: " << C.complex << "i" << endl;
return 0;
}3. Run-Time (Dynamic) Polymorphism
In runtime polymorphism, the compiler has no idea which function will run when it compiles the code. It is decided dynamically as the program runs.
The Early Binding Problem
It happens when a child class re-implements a method that it inherited from a parent class, using the exact same name and parameters.
The problem: When we create a child object (say Circle c), and take a parent pointer pointing to this object (Shape* s = &c), if we call s->draw(), we expect it to draw a Circle. But because of Early Binding, the compiler just looks at the pointer type (Shape*) and blindly calls the parent's Shape::draw().
The Fix: Late Binding (Virtual Keyword)
To fix this, we declare the base class method as virtual. Now, the compiler sees the virtual keyword and delays the decision until runtime (Late Binding). It checks what the pointer is actually pointing to in memory, and calls the child's method.
Runtime Polymorphism is exactly what happens when you use a parent class pointer to point to a child class object.
Upcasting vs Downcasting
- Upcasting: When a child object is being pointed to by a parent pointer (
Shape *s2 = new Circle();). This is safe and implicit. - Downcasting: Taking an upcasted parent pointer and converting it back into a child pointer. You must explicitly typecast this because C++ doesn't allow implicit downcasting (the parent shape could actually be a
Square, not aCircle).
class Shape {
public:
Shape() {}
virtual void draw() {
cout << "Drawing some shape..." << endl;
}
virtual ~Shape() {}
};
class Circle : public Shape {
public:
// 'override' makes code more readable and catches typos
// 'final' prevents further overriding in grandchild classes
void draw() override final {
cout << "Drawing Circle..." << endl;
}
~Circle() override {
cout << "Circle Destructor" << endl;
}
};
int main() {
// 1. Stack allocation
Shape s;
Circle c;
s.draw(); // "Drawing some shape..."
c.draw(); // "Drawing Circle..."
// 2. Upcasting (The core of runtime polymorphism)
Shape *s2 = new Circle();
// Because draw() is virtual, this calls Circle's draw!
// Without virtual, this would output "Drawing some shape..."
s2->draw();
delete s2;
// 3. Downcasting
Shape *s3 = new Circle();
// Need to explicitly typecast to (Circle*)
Circle *c2 = (Circle *)s3;
c2->draw();
delete s3;
return 0;
}4. Deep Dive: Virtual Destructors & Memory Leaks
If a class has any virtual functions, its destructor must be virtual.
Why?
Look at the upcasting example: Shape *s2 = new Circle(); delete s2;.
If ~Shape() is NOT virtual, the compiler uses early binding for the destructor. It just sees a Shape* and calls ~Shape(). The Circle part of the object is never destroyed, leading to a massive memory leak!
By making it virtual ~Shape(), late binding is used, calling ~Circle() first, which then automatically calls ~Shape(), cleaning up perfectly.
5. The final Keyword
The final keyword is used in two ways:
- With classes to prevent inheritance: If you declare
class Car final {};, then that class cannot be inherited by any other class. - With virtual methods to prevent overriding: If you declare
void print() override final {};, it prevents theprintmethod from being overridden by any further child classes down the hierarchy.