Understanding C++ Polymorphism: Static and Dynamic Binding

Polymorphism Categories

Static polymorphism: Achieved through function overloading and operator overloading, reusing the function name.
Dynamic polymorphism: Achieved through derived classes and virtual functions, enabling runtime polymorphism.

The key difference:
● In static polymorphism, the function address is bound early – determined at compile time.
● In dynamic polymorphism, the function address is bound late – determined at runtime.

#include <iostream>
using namespace std;

class Creature {
public:
   // The vocalize function is virtual.
   // Adding the virtual keyword means the compiler cannot resolve the function call at compile time.
   virtual void vocalize() {
       cout << "Creature makes a sound" << endl;
   }
};

class Feline : public Creature {
public:
   void vocalize() override {   // override specifier for clarity
       cout << "Meow" << endl;
   }
};

class Canine : public Creature {
public:
   void vocalize() override {
       cout << "Bark" << endl;
   }
};

// We want the appropriate object's function to be called.
// If the function address is fixed at compile time → static binding (early binding).
// If it is resolved at runtime → dynamic binding (late binding).
// The parameter must be a reference (or pointer) to enable dynamic binding.
void makeSound(Creature& creature) {
   creature.vocalize();
}

int main() {
   Feline cat;
   makeSound(cat);    // outputs "Meow"

   Canine dog;
   makeSound(dog);    // outputs "Bark"

   return 0;
}</iostream>

In makeSound, the parameter is Creature& – a reference to the base class. If we removed the reference and passed by value, the function would always call Creature::vocalize() (static binding), regardlesss of the actual object passed. By using a reference (or pointer), the call is resolved at runtime based on the actual type of the object. This is dynamic binding.

Summary of requirements for polymorphism:
● An inheritance relationship must exist.
● The derived class must override a virtual function of the base class.
Usage condition for polymorphism:
● A base-class pointer or reference must point to (or refer to) a derived-class object.
Overriding: the return type, function name, and parameter list must be exactly the same.

Polymorphism Example: A Simple Calculator

#include <iostream>
using namespace std;

// Non-polymorphic implementation
class Calculator {
public:
   int compute(const string& op) {
       if (op == "+") {
           return valueA + valueB;
       } else if (op == "-") {
           return valueA - valueB;
       } else if (op == "*") {
           return valueA * valueB;
       }
       // Adding a new operation requires modifying this source code.
       return 0;
   }

   int valueA;
   int valueB;
};

void testNonPoly() {
   Calculator calc;
   calc.valueA = 10;
   calc.valueB = 5;
   cout << calc.valueA << " + " << calc.valueB << " = " << calc.compute("+") << endl;
   cout << calc.valueA << " - " << calc.valueB << " = " << calc.compute("-") << endl;
   cout << calc.valueA << " * " << calc.valueB << " = " << calc.compute("*") << endl;
}

// Polymorphic implementation
// Abstract base class for a binary operation
// Advantages: clearer organization, high readability, easier extension and maintenance.
class AbstractOperation {
public:
   virtual int execute() = 0;
   int operandA;
   int operandB;
};

class Addition : public AbstractOperation {
public:
   int execute() override {
       return operandA + operandB;
   }
};

class Subtraction : public AbstractOperation {
public:
   int execute() override {
       return operandA - operandB;
   }
};

class Multiplication : public AbstractOperation {
public:
   int execute() override {
       return operandA * operandB;
   }
};

void testPoly() {
   AbstractOperation* op = new Addition();
   op->operandA = 10;
   op->operandB = 5;
   cout << op->operandA << " + " << op->operandB << " = " << op->execute() << endl;
   delete op;   // clean up

   op = new Subtraction();
   op->operandA = 10;
   op->operandB = 5;
   cout << op->operandA << " - " << op->operandB << " = " << op->execute() << endl;
   delete op;

   op = new Multiplication();
   op->operandA = 10;
   op->operandB = 5;
   cout << op->operandA << " * " << op->operandB << " = " << op->execute() << endl;
   delete op;
}

int main() {
   // testNonPoly();
   testPoly();
   return 0;
}</iostream>

Pure Virtual Functions and Abstract Classes

In polymorphic designs, the base class implementation of a virtual function is often meaningless; the actual work is done by the derived classes. Such functions can be declared as pure virtual functions.

Syntax: virtual ReturnType functionName(parameters) = 0;
A class containing atleast one pure virtual function becomes an abstract class.

Characteristics of abstract classes:
● Objects cannot be instantiated directly.
● Derived classes must override all pure virtual functions; otherwise, they also become abstract.

#include <iostream>
using namespace std;

class Base {
public:
   // Pure virtual function makes this an abstract class.
   // Abstract classes cannot be instantiated.
   // Derived classes must override this function, or they remain abstract.
   virtual void process() = 0;
};

class Derived : public Base {
public:
   void process() override {
       cout << "process() invoked" << endl;
   }
};

int main() {
   Base* ptr = nullptr;
   // ptr = new Base;   // Error: cannot instantiate abstract class
   ptr = new Derived();
   ptr->process();
   delete ptr;   // free memory
   return 0;
}</iostream>

Practical Use Case: Preparing Beverages

#include <iostream>
using namespace std;

// Abstract class representing a beverage preparation process
class AbstractBeverage {
public:
   virtual void heatWater() = 0;
   virtual void infuse() = 0;
   virtual void pourIntoCup() = 0;
   virtual void addCondiments() = 0;

   // Template method defining the overall procedure
   void prepare() {
       heatWater();
       infuse();
       pourIntoCup();
       addCondiments();
   }
};

// Coffee preparation
class Coffee : public AbstractBeverage {
public:
   void heatWater() override {
       cout << "Heating filtered water!" << endl;
   }
   void infuse() override {
       cout << "Brewing coffee grounds!" << endl;
   }
   void pourIntoCup() override {
       cout << "Pouring coffee into the cup!" << endl;
   }
   void addCondiments() override {
       cout << "Adding milk and sugar!" << endl;
   }
};

// Tea preparation
class Tea : public AbstractBeverage {
public:
   void heatWater() override {
       cout << "Boiling tap water!" << endl;
   }
   void infuse() override {
       cout << "Steeping tea leaves!" << endl;
   }
   void pourIntoCup() override {
       cout << "Pouring tea into the cup!" << endl;
   }
   void addCondiments() override {
       cout << "Adding honey and lemon!" << endl;
   }
};

// Client function using the base-class pointer
void serveBeverage(AbstractBeverage* beverage) {
   beverage->prepare();
   delete beverage;
}

int main() {
   serveBeverage(new Coffee());
   serveBeverage(new Tea());
   return 0;
}</iostream>

Virtual Destructors and Pure Virtual Destructors

When a derived class allocates memory on the heap, deleting an object through a base-class pointer may not invoke the derived class destructor, causing memory leaks.

Solution: make the base class destructor virtual or pure virtual.

Commonalities:
● Both enable proper cleanup of derived objects when deleted through a base pointer.
● Both require an actual function definition (even pure virtual destructors need an implementation).

Differences:
● A class with a pure virtual destructor is abstract and cannot be instantiated.

Virtual destructor syntax:
virtual ~ClassName() {}

Pure virtual destructor syntax:
virtual ~ClassName() = 0;
ClassName::~ClassName() {} // definition required outside the class

#include <iostream>
using namespace std;

class Creature {
public:
   Creature() {
       cout << "Creature constructor called!" << endl;
   }

   virtual void speak() = 0;   // pure virtual function

   // Pure virtual destructor – makes Creature abstract
   virtual ~Creature() = 0;
};

// Definition of the pure virtual destructor
Creature::~Creature() {
   cout << "Creature pure virtual destructor called!" << endl;
}

class Feline : public Creature {
public:
   Feline(string name) {
       cout << "Feline constructor called!" << endl;
       petName = new string(name);
   }

   void speak() override {
       cout << *petName << " the cat says meow!" << endl;
   }

   ~Feline() {
       cout << "Feline destructor called!" << endl;
       if (petName != nullptr) {
           delete petName;
           petName = nullptr;
       }
   }

private:
   string* petName;   // heap-allocated memory in derived class
};

int main() {
   Creature* creature = new Feline("Tom");
   creature->speak();

   // Deleting through base pointer: if destructor were not virtual,
   // the Feline destructor wouldn't be called, causing a memory leak.
   delete creature;   

   return 0;
}</iostream>

Key takeaways:
● Virtual (or pure virtual) destructors ensure proper cleanup when deleting derived objects through a base pointer.
● If a derived class does not allocate heap resources, a virtual destructor is not strictly required.
● A class with a pure virtual destructor is abstract and cannot be instantiated directly.

Tags: C++ Polymorphism Virtual Functions abstract classes Dynamic Binding

Posted on Mon, 14 Sep 2026 16:41:36 +0000 by ryanbutler