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 virtual keyword.
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 virtual keyword.
  • 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 / SymbolPurposeUnder-The-Hood Impact
virtualDelays function binding until runtime.Allocates a hidden virtual pointer (vptr) pointing to a class virtual method table (vtable).
overrideDerived class specifier confirming virtual function override.Prevents silent signature mismatch bugs at compile-time.
= 0Declares 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 and vptr. Dynamic binding requires passing by pointer or reference (Base& or Base*).

Trap 2: Virtual Constructors Do Not Exist

Constructors cannot be virtual because the vptr is 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).