Lesson 7: Dynamic Binding & Runtime Polymorphism
Lesson Overview
This lesson covers the most conceptually rich topic in C++ OOP: Polymorphism. We will break down how C++ shifts function resolution from compile-time to runtime, dissect how base-class pointers navigate derived objects, uncover the hidden memory layout of virtual dispatch tables (
vtable/vptr), and address the critical resource leakages solved by virtual destructors.
π 1. The βWhyβ & Concept Breakdown
At its core, polymorphism {the ability of different classes to respond to the same function call in their own unique way} means βmany forms.β In C++, it allows a single interface (a base class pointer or reference) to trigger different behaviors depending on the actual type of the object it points to at runtime.
Static vs. Dynamic Binding
- Static (Early) Binding: Resolved at compile-time {compiler locks in function address during build} based entirely on the declared type of the pointer. Fast but rigid.
- Dynamic (Late) Binding: Resolved at runtime {deferred until program execution} based on the actual object type being pointed to in RAM, enabled by the
virtualkeyword.
graph TD subgraph EarlyBinding ["Static / Early Binding (No virtual)"] P1["Base Pointer (Base*)"] -->|Points to| O1["Derived Object"] Call1["ptr->display()"] -->|Compiles directly to| F1["Base::display()"] end subgraph LateBinding ["Dynamic / Late Binding (With virtual)"] P2["Base Pointer (Base*)"] -->|Points to| O2["Derived Object"] Call2["ptr->display()"] -->|Looks up vtable at runtime| F2["Derived::display()"] end
β‘ Polymorphism: Python vs. C++
- Static vs. Dynamic: Python methods are dynamic/duck-typed by default. C++ defaults to static compile-time binding; dynamic dispatch requires the
virtualkeyword.- Abstract Classes: C++ pure virtual functions (
virtual void func() = 0;) correspond to Pythonβs@abstractmethod.- Virtual Destructors: Deleting derived objects via base pointers requires
virtual ~Base()to prevent leaking child heap resources.
The Base Class Pointer Rule
A pointer of type Base* is permitted to point to any object of class Derived (upcasting). By default, early binding restricts the pointer to invoking Base methods. Marking methods virtual unlocks runtime dispatch to the derived classβs implementation.
π 2. Keyword & Syntax Dictionary
| Keyword / Symbol | Purpose | Under-The-Hood Impact |
|---|---|---|
virtual | Delays function binding until runtime. | Allocates a hidden virtual pointer (vptr) pointing to a class virtual method table (vtable). |
override | Derived class specifier confirming virtual function override. | Prevents silent signature mismatch bugs at compile-time. |
= 0 | Declares a Pure Virtual Function (no base implementation). | Makes the class an Abstract Class (cannot be instantiated directly). |
virtual ~Base() | Virtual Destructor: Ensures derived destructors run during polymorphic deletions. | Prevents severe heap memory leaks when deleting derived objects via base pointers. |
π» 3. Step-by-Step Code Evolution
Step 7.1: Virtual Functions in Action
#include <iostream>
using namespace std;
class Hero {
public:
// virtual enables dynamic runtime dispatch
virtual void attack() {
cout << "Hero swings a basic sword!" << endl;
}
virtual ~Hero() {} // Virtual destructor
};
class Mage : public Hero {
public:
void attack() override {
cout << "Mage casts a devastating Fireball!" << endl;
}
};
class Archer : public Hero {
public:
void attack() override {
cout << "Archer fires a piercing Arrow!" << endl;
}
};
int main() {
Hero* party[2];
party[0] = new Mage();
party[1] = new Archer();
// Bound at runtime based on actual object type!
party[0]->attack(); // Outputs: Mage casts a devastating Fireball!
party[1]->attack(); // Outputs: Archer fires a piercing Arrow!
delete party[0];
delete party[1];
return 0;
}Step 7.2: Abstract Classes & Pure Virtual Functions
#include <iostream>
using namespace std;
// Abstract Base Class (Cannot instantiate directly)
class Shape {
protected:
string name;
public:
Shape(string n) : name(n) {}
// Pure Virtual Function
virtual double calculateArea() = 0;
virtual void display() {
cout << "Shape: " << name << " | Area: " << calculateArea() << endl;
}
virtual ~Shape() {}
};
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : Shape("Circle"), radius(r) {}
double calculateArea() override {
return 3.14159 * radius * radius;
}
};
int main() {
// Shape s; // ERROR: Cannot instantiate abstract class
Shape* sPtr = new Circle(5.0);
sPtr->display(); // Polymorphic invocation
delete sPtr;
return 0;
}Step 7.3: The Memory Leak Bug & Virtual Destructor Fix
#include <iostream>
using namespace std;
class Base {
public:
Base() { cout << "[+] Base Constructed" << endl; }
// VIRTUAL DESTRUCTOR: Mandatory for polymorphic base classes
virtual ~Base() { cout << "[-] Base Destructed" << endl; }
};
class Derived : public Base {
private:
int* heapArray;
public:
Derived() {
cout << "[+] Derived Constructed (Allocating 100 ints)" << endl;
heapArray = new int[100];
}
~Derived() override {
cout << "[-] Derived Destructed (Freeing 100 ints)" << endl;
delete[] heapArray;
}
};
int main() {
Base* ptr = new Derived();
cout << "\n--- Deleting Polymorphically ---" << endl;
delete ptr; // Properly runs ~Derived() THEN ~Base()
return 0;
}Output:
[+] Base Constructed
[+] Derived Constructed (Allocating 100 ints)
--- Deleting Polymorphically ---
[-] Derived Destructed (Freeing 100 ints)
[-] Base Destructedπ§ 4. Under-the-Hood: VTable & VPtr Architecture
When a class declares or inherits virtual functions, the compiler automatically attaches a hidden pointer called vptr to the object:
HEAP MEMORY (Object Space)
[ Mage Object ]
βββ vptr βββββββββββββββββββββββ (Hidden pointer inserted by compiler, 8 bytes)
βββ hp = 100 β
βββ mana = 250 β
β
STATIC READ-ONLY MEMORY βΌ
[ Mage Class VTable ]
βββ &Mage::attack() βββββββββββΊ Points to Mage's attack implementation
βββ &Hero::defend() βββββββββββΊ Points to inherited Hero defend method
Memory Impact
- Time Overhead: 1 extra pointer dereference per polymorphic call.
- Space Overhead: 8 bytes per object instance on 64-bit systems to store the
vptr.
β οΈ 5. The Debuggerβs Guide (Common Traps)
Trap 1: The Object Slicing Trap
Passing a derived object by value to a base parameter (
void test(Base obj)) copies only the Base portion, slicing off derived attributes andvptr. Dynamic binding requires passing by pointer or reference (Base&orBase*).
Trap 2: Virtual Constructors Do Not Exist
Constructors cannot be virtual because the
vptris initialized by the constructor itself during object instantiation.
Trap 3: Calling Virtual Functions inside Constructors/Destructors
During construction of a base subobject, the derived portion has not yet been built. C++ disables dynamic binding inside constructors and destructors to prevent accessing uninitialized derived members.
π¬ 6. Viva Quick-Prep
Q1: What is an Abstract Class and how is it created?
Answer: An abstract class serves as a conceptual interface that cannot be instantiated directly. It is created by declaring at least one Pure Virtual Function (virtual void func() = 0;).
Q2: Why must a base class destructor be declared virtual?
Answer: If the base destructor is non-virtual, deleting a derived object via a base pointer uses early binding, invoking only ~Base(). The derived destructor is skipped, leaking any heap memory owned by the derived object.
Q3: What is the difference between Function Overloading and Function Overriding?
Answer: Overloading occurs within the same scope where multiple functions share a name but differ in signatures (resolved at compile-time). Overriding occurs in a derived class redefining an exact virtual base function signature (resolved at runtime).