Understanding Polymorphism in C++ Object-Oriented Programming
Polymorphism is a core principle of object-oriented programming in C++. It enables different behaviors through a common interface.
There are two primary forms of polymorphism:
- Static Polymorphism (Early Binding): Achieved through function overloading and operator overloading. The function address is resolved during compilation.
- Dynamic Polymorphism (Late Binding): Achieved through inheritance and virtual functions. The function address is resolved during program execution.
The following example demonstrates static polymorphism and its limitation:
#include <iostream>
using std::cout;
using std::endl;
class Creature {
public:
void communicate() {
cout << "Creature makes a sound." << endl;
}
};
class Lion : public Creature {
public:
void communicate() {
cout << "The lion roars." << endl;
}
};
class Elephant : public Creature {
public:
void communicate() {
cout << "The elephant trumpets." << endl;
}
};
void makeSound(Creature &c) {
c.communicate(); // Calls Creature::communicate() due to early binding
}
int main() {
Lion simba;
Elephant dumbo;
makeSound(simba); // Output: Creature makes a sound.
makeSound(dumbo); // Output: Creature makes a sound.
return 0;
}
Because the makeSound function uses the base class reference and the method is not virtual, the function call is bound at compile time to Creature::communicate().
To achieve dynamic polymorphism, we use virtual functions. A virtual function in a base class signals that derived classes can provide their own implementation.
#include <iostream>
using std::cout;
using std::endl;
class Creature {
public:
virtual void communicate() { // Declare as virtual
cout << "Creature makes a sound." << endl;
}
};
class Lion : public Creature {
public:
void communicate() override { // Override the virtual function
cout << "The lion roars." << endl;
}
};
class Elephant : public Creature {
public:
void communicate() override {
cout << "The elephant trumpets." << endl;
}
};
void makeSound(Creature &c) {
c.communicate(); // Calls the appropriate derived class function at runtime
}
int main() {
Lion simba;
Elephant dumbo;
makeSound(simba); // Output: The lion roars.
makeSound(dumbo); // Output: The elephant trumpets.
return 0;
}
Conditions for Dynamic Polymorphism:
- An inheritance relationship.
- The base class function must be declared
virtual. - The derived class must override the base class virtual function (matching function signature).
- A base class pointer or reference must be used to invoke the function on a derived class object.
Mechanics of Virtual Functions
When a class contains at least one virtual function, the compiler adds a hidden member to it: a virtual table pointer (vptr). This vptr points to a virtual table (vtable), which is an array of function pointers. Each entry in the vtable corresponds to the address of a virtual function for that class.
- A base class object's vptr points to its own vtable.
- A derived class object inherits the base class's vptr, wich initially points to the base class's vtable.
- When the derived class overrides a virtual function, the corresponding entry in its vtable is updated to point to the derived class's implementation. When a virtual function is called through a base class pointer/reference, the program follows the object's vptr to the correct vtable and calls the function whose address is stored there.
Abstract Classes and Pure Virtual Functions
Sometimes, a base class's virtual function serves only as a placeholder, with no meaningful default implementation. Such functions can be declared as pure virtual functions.
Syntax: virtual ReturnType FunctionName(Parameters) = 0;
A class containing at least one pure virtual function is an abstract class.
Properties of Abstract Classes:
- Cannot be instantiated.
- Derived classes must override all pure virtual functions; otherwise, they remain abstract.
#include <iostream>
using std::cout;
using std::endl;
class Shape { // Abstract class
public:
virtual void draw() = 0; // Pure virtual function
};
class Circle : public Shape {
public:
void draw() override {
cout << "Drawing a circle." << endl;
}
};
int main() {
// Shape s; // Error: Cannot instantiate abstract class
Shape* ptr = new Circle();
ptr->draw(); // Output: Drawing a circle.
delete ptr;
return 0;
}
Virtual Destructors
When using polymorphism with base class pointers, a critical issue arises if the derived class allocates memory on the heap. Deleting the object through a base class pointer may only invoke the base class destructor, leading to memory leaks in the derived class.
Solution: Declare the base class destructor as virtual.
#include <iostream>
#include <string>
using std::cout;
using std::endl;
using std::string;
class Entity {
public:
Entity() { cout << "Entity constructed." << endl; }
virtual ~Entity() { cout << "Entity destroyed." << endl; } // Virtual destructor
virtual void identify() = 0;
};
class Character : public Entity {
string* name;
public:
Character(string n) {
cout << "Character constructed." << endl;
name = new string(n); // Heap allocation
}
void identify() override {
cout << "Character: " << *name << endl;
}
~Character() {
cout << "Character destroyed." << endl;
delete name; // Cleanup heap memory
}
};
int main() {
Entity* e = new Character("Hero");
e->identify();
delete e; // Correctly calls Character::~Character() then Entity::~Entity()
return 0;
}
A destructor can also be declared as a pure virtual destructor (virtual ~ClassName() = 0;). This makes the class abstract, but unlike other pure virtual functions, a pure virtual destructor must have an implementation (typically defined outside the class). This is necessary because destructors in a hierarchy are called in reverse order.
class Base {
public:
virtual ~Base() = 0; // Pure virtual destructor declaration
};
Base::~Base() { // Pure virtual destructor definition
// Cleanup for Base class
}
- Use virtual destructors when a class is designed to be a polymorphic base class, especially if derived classes manage their own resources.