Module 5.3

C++ Polymorphism

Discover the power of polymorphism in C++. Learn how virtual functions enable runtime behavior selection, how to create flexible class hierarchies with abstract classes, and how to write code that works with objects of different types through a common interface.

50 min read
Intermediate
Hands-on Examples
What You'll Learn
  • Virtual functions and dynamic binding
  • Function overriding with override keyword
  • Abstract classes and pure virtual functions
  • Runtime vs compile-time polymorphism
  • Virtual destructors and best practices
Contents
01

Introduction to Polymorphism

Polymorphism is one of the four pillars of Object-Oriented Programming. The word comes from Greek, meaning "many forms." In C++, polymorphism allows objects of different classes to be treated as objects of a common base class, with each object responding to the same method call in its own way.

OOP Pillar

Polymorphism

Polymorphism allows a single interface to represent different underlying data types. A base class pointer or reference can point to derived class objects, and the correct method is called based on the actual object type at runtime.

Key Benefit: Write code that works with the base class, and it automatically works with all derived classes - even those created in the future.

// Without polymorphism - tedious type checking
void makeSound(Animal* animal, string type) {
    if (type == "dog") {
        cout << "Woof!" << endl;
    } else if (type == "cat") {
        cout << "Meow!" << endl;
    } else if (type == "cow") {
        cout << "Moo!" << endl;
    }
    // Must add new else-if for every new animal!
}

Without polymorphism, you need manual type checking for every operation. This approach is error-prone, hard to maintain, and violates the Open-Closed Principle - you must modify existing code every time you add a new animal type.

// With polymorphism - elegant and extensible
class Animal {
public:
    virtual void speak() {
        cout << "Some sound" << endl;
    }
};

class Dog : public Animal {
public:
    void speak() override {
        cout << "Woof!" << endl;
    }
};

class Cat : public Animal {
public:
    void speak() override {
        cout << "Meow!" << endl;
    }
};

With polymorphism, each class defines its own speak() method. The virtual keyword enables dynamic binding, and override ensures we are correctly overriding the base class method. New animal types can be added without changing existing code.

void makeSound(Animal* animal) {
    animal->speak();  // Correct method called automatically!
}

int main() {
    Dog dog;
    Cat cat;
    
    makeSound(&dog);  // Output: Woof!
    makeSound(&cat);  // Output: Meow!
    
    return 0;
}

The makeSound() function works with any Animal pointer. At runtime, C++ determines the actual object type and calls the appropriate speak() method. This is called dynamic dispatch or late binding.

Benefits of Polymorphism
  • Write flexible, reusable code
  • Easy to extend with new types
  • Simplifies complex conditional logic
  • Supports the Open-Closed Principle
Things to Remember
  • Requires pointers or references
  • Small runtime overhead (vtable lookup)
  • Base class needs virtual destructor
  • Cannot be used with value semantics

Practice Questions: Introduction to Polymorphism

Task: Create a Shape base class with a virtual area() method. Create Rectangle and Circle derived classes that override area().

Show Solution
#include <iostream>
using namespace std;

class Shape {
public:
    virtual double area() {
        return 0;
    }
};

class Rectangle : public Shape {
private:
    double width, height;
public:
    Rectangle(double w, double h) : width(w), height(h) {}
    
    double area() override {
        return width * height;
    }
};

class Circle : public Shape {
private:
    double radius;
public:
    Circle(double r) : radius(r) {}
    
    double area() override {
        return 3.14159 * radius * radius;
    }
};

int main() {
    Rectangle rect(5, 3);
    Circle circle(4);
    
    Shape* shapes[] = {&rect, &circle};
    
    for (Shape* s : shapes) {
        cout << "Area: " << s->area() << endl;
    }
    return 0;
}

Task: Create an Employee base class with virtual calculatePay(). Create SalariedEmployee (fixed monthly) and HourlyEmployee (hours * rate) derived classes.

Show Solution
#include <iostream>
#include <string>
using namespace std;

class Employee {
protected:
    string name;
public:
    Employee(string n) : name(n) {}
    
    virtual double calculatePay() {
        return 0;
    }
    
    string getName() { return name; }
};

class SalariedEmployee : public Employee {
private:
    double monthlySalary;
public:
    SalariedEmployee(string n, double salary) 
        : Employee(n), monthlySalary(salary) {}
    
    double calculatePay() override {
        return monthlySalary;
    }
};

class HourlyEmployee : public Employee {
private:
    double hourlyRate;
    int hoursWorked;
public:
    HourlyEmployee(string n, double rate, int hours) 
        : Employee(n), hourlyRate(rate), hoursWorked(hours) {}
    
    double calculatePay() override {
        return hourlyRate * hoursWorked;
    }
};

int main() {
    SalariedEmployee manager("Alice", 5000);
    HourlyEmployee worker("Bob", 25, 160);
    
    Employee* employees[] = {&manager, &worker};
    
    for (Employee* e : employees) {
        cout << e->getName() << ": $" << e->calculatePay() << endl;
    }
    return 0;
}

Task: Create a Notification base class with virtual send(string message). Create EmailNotification, SMSNotification, and PushNotification derived classes. Create a NotificationManager that can send to multiple notification channels at once.

Show Solution
#include <iostream>
#include <string>
#include <vector>
using namespace std;

class Notification {
public:
    virtual void send(string message) {
        cout << "Sending notification: " << message << endl;
    }
    virtual ~Notification() {}
};

class EmailNotification : public Notification {
private:
    string email;
public:
    EmailNotification(string e) : email(e) {}
    
    void send(string message) override {
        cout << "[EMAIL to " << email << "] " << message << endl;
    }
};

class SMSNotification : public Notification {
private:
    string phone;
public:
    SMSNotification(string p) : phone(p) {}
    
    void send(string message) override {
        cout << "[SMS to " << phone << "] " << message << endl;
    }
};

class PushNotification : public Notification {
private:
    string deviceId;
public:
    PushNotification(string d) : deviceId(d) {}
    
    void send(string message) override {
        cout << "[PUSH to device " << deviceId << "] " << message << endl;
    }
};

class NotificationManager {
private:
    vector<Notification*> channels;
public:
    void addChannel(Notification* channel) {
        channels.push_back(channel);
    }
    
    void broadcast(string message) {
        cout << "Broadcasting to " << channels.size() << " channels:\n";
        for (Notification* channel : channels) {
            channel->send(message);
        }
    }
};

int main() {
    EmailNotification email("user@example.com");
    SMSNotification sms("+1-555-0123");
    PushNotification push("device-abc-123");
    
    NotificationManager manager;
    manager.addChannel(&email);
    manager.addChannel(&sms);
    manager.addChannel(&push);
    
    manager.broadcast("Your order has shipped!");
    
    return 0;
}
02

Virtual Functions

Virtual functions are the mechanism that enables runtime polymorphism in C++. When a function is declared as virtual in a base class, C++ uses dynamic binding to determine which version of the function to call based on the actual object type, not the pointer or reference type.

C++ Feature

Virtual Functions

A virtual function is a member function declared with the virtual keyword that can be overridden in derived classes. When called through a base class pointer or reference, the derived class version is executed if one exists.

Syntax: virtual return_type function_name(parameters);

// Without virtual - static binding (wrong behavior)
class Animal {
public:
    void speak() {  // Not virtual
        cout << "Animal sound" << endl;
    }
};

class Dog : public Animal {
public:
    void speak() {  // Hides base class method
        cout << "Woof!" << endl;
    }
};

int main() {
    Dog dog;
    Animal* ptr = &dog;
    
    ptr->speak();  // Output: "Animal sound" - WRONG!
    return 0;
}

Without the virtual keyword, C++ uses static binding. The compiler decides which function to call based on the pointer type (Animal*), not the actual object type (Dog). This is not the behavior we want for polymorphism.

// With virtual - dynamic binding (correct behavior)
class Animal {
public:
    virtual void speak() {  // Virtual keyword added
        cout << "Animal sound" << endl;
    }
};

class Dog : public Animal {
public:
    void speak() override {  // Overrides base class method
        cout << "Woof!" << endl;
    }
};

int main() {
    Dog dog;
    Animal* ptr = &dog;
    
    ptr->speak();  // Output: "Woof!" - CORRECT!
    return 0;
}

With virtual, C++ uses dynamic binding. At runtime, it checks the actual object type through the virtual table (vtable) and calls the correct method. The override keyword is optional but recommended - it helps catch errors if you accidentally misspell the function name.

// How virtual functions work internally (simplified)
// Each class with virtual functions has a vtable (virtual table)
// Each object has a hidden pointer (vptr) to its class's vtable

class Base {
public:
    virtual void func1() { cout << "Base::func1" << endl; }
    virtual void func2() { cout << "Base::func2" << endl; }
};

class Derived : public Base {
public:
    void func1() override { cout << "Derived::func1" << endl; }
    // func2 not overridden - uses Base::func2
};

// Base vtable: [&Base::func1, &Base::func2]
// Derived vtable: [&Derived::func1, &Base::func2]

Each class with virtual functions has a vtable containing pointers to its virtual functions. When a derived class overrides a function, its vtable entry points to the new implementation. Objects contain a hidden pointer (vptr) to their class's vtable, enabling runtime method lookup.

Important: Virtual functions have a small performance cost due to vtable lookup. Only use virtual when you need polymorphic behavior. For performance-critical code where polymorphism is not needed, prefer non-virtual functions.

Practice Questions: Virtual Functions

Task: Create a Printer class with non-virtual print() and virtual display(). Create ColorPrinter that overrides both. Test with base pointer and observe the difference.

Show Solution
#include <iostream>
using namespace std;

class Printer {
public:
    void print() {  // Non-virtual
        cout << "Printer: Black & White" << endl;
    }
    
    virtual void display() {  // Virtual
        cout << "Printer: Standard Display" << endl;
    }
};

class ColorPrinter : public Printer {
public:
    void print() {  // Hides base (not override)
        cout << "ColorPrinter: Full Color" << endl;
    }
    
    void display() override {  // True override
        cout << "ColorPrinter: HD Display" << endl;
    }
};

int main() {
    ColorPrinter cp;
    Printer* ptr = &cp;
    
    ptr->print();    // "Printer: Black & White" (static binding)
    ptr->display();  // "ColorPrinter: HD Display" (dynamic binding)
    
    cp.print();      // "ColorPrinter: Full Color" (direct call)
    cp.display();    // "ColorPrinter: HD Display" (direct call)
    
    return 0;
}

Task: Create Vehicle with virtual describe() and maxSpeed(). Create Car, Motorcycle, and Bicycle that override both. Store in array and iterate.

Show Solution
#include <iostream>
#include <string>
using namespace std;

class Vehicle {
public:
    virtual string describe() {
        return "Generic Vehicle";
    }
    virtual int maxSpeed() {
        return 0;
    }
};

class Car : public Vehicle {
public:
    string describe() override {
        return "Four-wheeled automobile";
    }
    int maxSpeed() override {
        return 200;
    }
};

class Motorcycle : public Vehicle {
public:
    string describe() override {
        return "Two-wheeled motor vehicle";
    }
    int maxSpeed() override {
        return 180;
    }
};

class Bicycle : public Vehicle {
public:
    string describe() override {
        return "Human-powered two-wheeler";
    }
    int maxSpeed() override {
        return 40;
    }
};

int main() {
    Car car;
    Motorcycle moto;
    Bicycle bike;
    
    Vehicle* vehicles[] = {&car, &moto, &bike};
    
    for (Vehicle* v : vehicles) {
        cout << v->describe() << " - Max: " 
             << v->maxSpeed() << " km/h" << endl;
    }
    return 0;
}

Task: Create a Plugin base with virtual getName(), getVersion(), and execute(). Create LoggerPlugin and SecurityPlugin. Create a PluginManager that stores and runs plugins.

Show Solution
#include <iostream>
#include <string>
#include <vector>
using namespace std;

class Plugin {
public:
    virtual string getName() = 0;
    virtual string getVersion() = 0;
    virtual void execute() = 0;
    virtual ~Plugin() {}
};

class LoggerPlugin : public Plugin {
public:
    string getName() override { return "Logger"; }
    string getVersion() override { return "1.0.0"; }
    void execute() override {
        cout << "[LOG] System running normally" << endl;
    }
};

class SecurityPlugin : public Plugin {
public:
    string getName() override { return "Security"; }
    string getVersion() override { return "2.1.0"; }
    void execute() override {
        cout << "[SECURITY] Scanning for threats..." << endl;
    }
};

class PluginManager {
private:
    vector<Plugin*> plugins;
public:
    void addPlugin(Plugin* p) {
        plugins.push_back(p);
        cout << "Loaded: " << p->getName() 
             << " v" << p->getVersion() << endl;
    }
    
    void runAll() {
        for (Plugin* p : plugins) {
            p->execute();
        }
    }
};

int main() {
    LoggerPlugin logger;
    SecurityPlugin security;
    
    PluginManager manager;
    manager.addPlugin(&logger);
    manager.addPlugin(&security);
    
    cout << "\nRunning all plugins:\n";
    manager.runAll();
    
    return 0;
}

Task: Create FileSystemItem with virtual getName(), getSize(), and display(int indent). Create File and Folder classes. Folders should contain other items and calculate total size recursively.

Show Solution
#include <iostream>
#include <string>
#include <vector>
using namespace std;

class FileSystemItem {
protected:
    string name;
public:
    FileSystemItem(string n) : name(n) {}
    virtual string getName() { return name; }
    virtual int getSize() = 0;
    virtual void display(int indent = 0) = 0;
    virtual ~FileSystemItem() {}
};

class File : public FileSystemItem {
private:
    int size;
public:
    File(string n, int s) : FileSystemItem(n), size(s) {}
    
    int getSize() override { return size; }
    
    void display(int indent = 0) override {
        for (int i = 0; i < indent; i++) cout << "  ";
        cout << "File: " << name << " (" << size << " KB)" << endl;
    }
};

class Folder : public FileSystemItem {
private:
    vector<FileSystemItem*> items;
public:
    Folder(string n) : FileSystemItem(n) {}
    
    void addItem(FileSystemItem* item) {
        items.push_back(item);
    }
    
    int getSize() override {
        int total = 0;
        for (FileSystemItem* item : items) {
            total += item->getSize();
        }
        return total;
    }
    
    void display(int indent = 0) override {
        for (int i = 0; i < indent; i++) cout << "  ";
        cout << "Folder: " << name << " (" << getSize() << " KB total)" << endl;
        for (FileSystemItem* item : items) {
            item->display(indent + 1);
        }
    }
};

int main() {
    File file1("document.txt", 25);
    File file2("image.jpg", 150);
    File file3("video.mp4", 5000);
    
    Folder subfolder("Photos");
    subfolder.addItem(&file2);
    
    Folder root("MyDocuments");
    root.addItem(&file1);
    root.addItem(&subfolder);
    root.addItem(&file3);
    
    root.display();
    
    return 0;
}
03

Function Overriding

Function overriding occurs when a derived class provides a specific implementation for a virtual function that is already defined in its base class. The derived class version "overrides" the base class version, and the correct version is called based on the object's actual type at runtime.

OOP Concept

Function Overriding

Function overriding replaces a base class virtual function with a derived class implementation. The overriding function must have the same signature (name, parameters, and const qualifier) as the base class function.

Requirements: Same name, same parameters, same const-ness, base function must be virtual.

class Base {
public:
    virtual void greet() {
        cout << "Hello from Base" << endl;
    }
    
    virtual void greet(string name) {
        cout << "Hello, " << name << " from Base" << endl;
    }
};

class Derived : public Base {
public:
    // Correctly overrides greet()
    void greet() override {
        cout << "Hello from Derived" << endl;
    }
    
    // Correctly overrides greet(string)
    void greet(string name) override {
        cout << "Hello, " << name << " from Derived" << endl;
    }
};

Both overloaded versions of greet() can be overridden separately. The override keyword tells the compiler this function intends to override a base class virtual function. If no matching base function exists, the compiler will report an error, helping you catch mistakes early.

// The override keyword catches errors!
class Base {
public:
    virtual void process(int value) { }
};

class Derived : public Base {
public:
    // ERROR: override specified but does not override
    // void process(double value) override { }  // Wrong parameter type!
    
    // CORRECT: matches base signature
    void process(int value) override { }
};

Without override, the wrong signature would create a new function instead of overriding, leading to subtle bugs. The override keyword is a safety net that ensures your intention to override is correctly implemented.

// Calling base class version from derived class
class Animal {
public:
    virtual void speak() {
        cout << "Some animal sound" << endl;
    }
};

class Dog : public Animal {
public:
    void speak() override {
        Animal::speak();  // Call base class version first
        cout << "Woof! Woof!" << endl;
    }
};

int main() {
    Dog dog;
    dog.speak();
    // Output:
    // Some animal sound
    // Woof! Woof!
    return 0;
}

You can call the base class version using BaseClass::function() syntax. This is useful when you want to extend the base behavior rather than completely replace it. The derived class adds its own behavior while still utilizing the parent's implementation.

Aspect Overloading Overriding
Definition Same name, different parameters Same name, same parameters
Scope Same class Base and derived classes
Binding Compile-time (static) Runtime (dynamic)
Virtual Required No Yes (for polymorphism)

Practice Questions: Function Overriding

Task: Create Product with virtual display(). Create Book and Electronics that override it to show product-specific info.

Show Solution
#include <iostream>
#include <string>
using namespace std;

class Product {
protected:
    string name;
    double price;
public:
    Product(string n, double p) : name(n), price(p) {}
    
    virtual void display() {
        cout << "Product: " << name << " - $" << price << endl;
    }
};

class Book : public Product {
private:
    string author;
public:
    Book(string n, double p, string a) 
        : Product(n, p), author(a) {}
    
    void display() override {
        cout << "Book: " << name << " by " << author 
             << " - $" << price << endl;
    }
};

class Electronics : public Product {
private:
    int warrantyMonths;
public:
    Electronics(string n, double p, int w) 
        : Product(n, p), warrantyMonths(w) {}
    
    void display() override {
        cout << "Electronics: " << name << " - $" << price 
             << " (" << warrantyMonths << " month warranty)" << endl;
    }
};

int main() {
    Book book("C++ Primer", 49.99, "Lippman");
    Electronics laptop("MacBook Pro", 1999.99, 24);
    
    Product* products[] = {&book, &laptop};
    
    for (Product* p : products) {
        p->display();
    }
    return 0;
}

Task: Create Logger with virtual log(string). Create TimestampLogger that calls base log() but adds a timestamp prefix first.

Show Solution
#include <iostream>
#include <string>
#include <ctime>
using namespace std;

class Logger {
public:
    virtual void log(string message) {
        cout << "[LOG] " << message << endl;
    }
};

class TimestampLogger : public Logger {
public:
    void log(string message) override {
        time_t now = time(0);
        char* dt = ctime(&now);
        string timestamp(dt);
        timestamp.pop_back();  // Remove newline
        
        cout << "[" << timestamp << "] ";
        Logger::log(message);  // Call base class version
    }
};

class PriorityLogger : public Logger {
private:
    string priority;
public:
    PriorityLogger(string p) : priority(p) {}
    
    void log(string message) override {
        cout << "[" << priority << "] ";
        Logger::log(message);
    }
};

int main() {
    Logger basic;
    TimestampLogger timed;
    PriorityLogger urgent("URGENT");
    
    basic.log("System started");
    timed.log("User logged in");
    urgent.log("Disk space low!");
    
    return 0;
}

Task: Create Character with virtual attack() and defend(). Create Warrior, Mage, and Archer that override both methods. Each derived class should call the base class method first, then add its own behavior.

Show Solution
#include <iostream>
#include <string>
using namespace std;

class Character {
protected:
    string name;
    int health;
    int power;
public:
    Character(string n, int h, int p) : name(n), health(h), power(p) {}
    
    virtual void attack() {
        cout << name << " prepares to attack..." << endl;
    }
    
    virtual void defend() {
        cout << name << " takes a defensive stance..." << endl;
    }
    
    virtual ~Character() {}
};

class Warrior : public Character {
public:
    Warrior(string n) : Character(n, 150, 30) {}
    
    void attack() override {
        Character::attack();
        cout << "Warrior " << name << " swings sword! (Damage: " << power << ")" << endl;
    }
    
    void defend() override {
        Character::defend();
        cout << "Warrior " << name << " raises shield! (Block: 50%)" << endl;
    }
};

class Mage : public Character {
public:
    Mage(string n) : Character(n, 80, 50) {}
    
    void attack() override {
        Character::attack();
        cout << "Mage " << name << " casts fireball! (Damage: " << power << ")" << endl;
    }
    
    void defend() override {
        Character::defend();
        cout << "Mage " << name << " creates magic barrier! (Block: 30%)" << endl;
    }
};

class Archer : public Character {
public:
    Archer(string n) : Character(n, 100, 40) {}
    
    void attack() override {
        Character::attack();
        cout << "Archer " << name << " shoots arrow! (Damage: " << power << ")" << endl;
    }
    
    void defend() override {
        Character::defend();
        cout << "Archer " << name << " dodges quickly! (Block: 40%)" << endl;
    }
};

int main() {
    Warrior warrior("Thor");
    Mage mage("Gandalf");
    Archer archer("Legolas");
    
    Character* party[] = {&warrior, &mage, &archer};
    
    cout << "=== Battle Begins! ===\n\n";
    
    for (Character* c : party) {
        c->attack();
        cout << endl;
    }
    
    cout << "=== Enemy Attacks! ===\n\n";
    
    for (Character* c : party) {
        c->defend();
        cout << endl;
    }
    
    return 0;
}
04

Abstract Classes

Abstract classes define interfaces that derived classes must implement. They contain at least one pure virtual function - a function declared with = 0 that has no implementation in the base class. You cannot create objects of abstract classes directly.

OOP Concept

Abstract Class

An abstract class is a class with at least one pure virtual function. It serves as a blueprint for derived classes, defining what methods must exist without specifying how they work. Abstract classes cannot be instantiated directly.

Pure Virtual Syntax: virtual return_type function() = 0;

// Abstract class - cannot be instantiated
class Shape {
public:
    // Pure virtual function - no implementation
    virtual double area() = 0;
    virtual double perimeter() = 0;
    
    // Regular virtual function - has default implementation
    virtual void describe() {
        cout << "I am a shape" << endl;
    }
};

// Shape shape;  // ERROR! Cannot instantiate abstract class

The = 0 syntax declares a pure virtual function. Classes with pure virtual functions are abstract and cannot be instantiated. Any derived class must implement all pure virtual functions to become a concrete (non-abstract) class.

// Concrete class - implements all pure virtual functions
class Rectangle : public Shape {
private:
    double width, height;
public:
    Rectangle(double w, double h) : width(w), height(h) {}
    
    double area() override {
        return width * height;
    }
    
    double perimeter() override {
        return 2 * (width + height);
    }
};

class Circle : public Shape {
private:
    double radius;
public:
    Circle(double r) : radius(r) {}
    
    double area() override {
        return 3.14159 * radius * radius;
    }
    
    double perimeter() override {
        return 2 * 3.14159 * radius;
    }
};

Both Rectangle and Circle implement area() and perimeter(), making them concrete classes that can be instantiated. They inherit the default describe() implementation but could override it if needed.

int main() {
    Rectangle rect(5, 3);
    Circle circle(4);
    
    // Can use pointers/references to abstract type
    Shape* shapes[] = {&rect, &circle};
    
    for (Shape* s : shapes) {
        cout << "Area: " << s->area() << endl;
        cout << "Perimeter: " << s->perimeter() << endl;
        s->describe();
        cout << endl;
    }
    
    return 0;
}

While you cannot create Shape objects, you can use Shape pointers and references to work with derived classes polymorphically. This is the power of abstract classes - they define a common interface while allowing varied implementations.

Interface vs Abstract Class: C++ does not have a separate interface keyword like Java. Instead, you create an interface by making all functions pure virtual. This is sometimes called a "pure abstract class" or "interface class."
// Interface class (all pure virtual)
class Drawable {
public:
    virtual void draw() = 0;
    virtual void setColor(string color) = 0;
    virtual ~Drawable() {}  // Virtual destructor is important!
};

class Button : public Drawable {
private:
    string label;
    string color;
public:
    Button(string l) : label(l), color("gray") {}
    
    void draw() override {
        cout << "Drawing " << color << " button: " << label << endl;
    }
    
    void setColor(string c) override {
        color = c;
    }
};

Practice Questions: Abstract Classes

Task: Create abstract Animal with pure virtual speak() and move(). Create Bird and Fish concrete classes.

Show Solution
#include <iostream>
using namespace std;

class Animal {
public:
    virtual void speak() = 0;
    virtual void move() = 0;
    virtual ~Animal() {}
};

class Bird : public Animal {
public:
    void speak() override {
        cout << "Tweet tweet!" << endl;
    }
    
    void move() override {
        cout << "Flying through the air" << endl;
    }
};

class Fish : public Animal {
public:
    void speak() override {
        cout << "Blub blub!" << endl;
    }
    
    void move() override {
        cout << "Swimming in water" << endl;
    }
};

int main() {
    Bird sparrow;
    Fish goldfish;
    
    Animal* animals[] = {&sparrow, &goldfish};
    
    for (Animal* a : animals) {
        a->speak();
        a->move();
        cout << endl;
    }
    
    return 0;
}

Task: Create interface PaymentProcessor with pure virtual processPayment(double) and refund(double). Implement CreditCard and PayPal processors.

Show Solution
#include <iostream>
#include <string>
using namespace std;

class PaymentProcessor {
public:
    virtual bool processPayment(double amount) = 0;
    virtual bool refund(double amount) = 0;
    virtual string getName() = 0;
    virtual ~PaymentProcessor() {}
};

class CreditCard : public PaymentProcessor {
private:
    string cardNumber;
public:
    CreditCard(string num) : cardNumber(num) {}
    
    bool processPayment(double amount) override {
        cout << "Processing $" << amount 
             << " via Credit Card ending in " 
             << cardNumber.substr(cardNumber.length() - 4) << endl;
        return true;
    }
    
    bool refund(double amount) override {
        cout << "Refunding $" << amount << " to Credit Card" << endl;
        return true;
    }
    
    string getName() override { return "Credit Card"; }
};

class PayPal : public PaymentProcessor {
private:
    string email;
public:
    PayPal(string e) : email(e) {}
    
    bool processPayment(double amount) override {
        cout << "Processing $" << amount 
             << " via PayPal (" << email << ")" << endl;
        return true;
    }
    
    bool refund(double amount) override {
        cout << "Refunding $" << amount << " to PayPal" << endl;
        return true;
    }
    
    string getName() override { return "PayPal"; }
};

int main() {
    CreditCard visa("4111111111111234");
    PayPal paypal("user@email.com");
    
    PaymentProcessor* processors[] = {&visa, &paypal};
    
    for (PaymentProcessor* p : processors) {
        cout << "Using " << p->getName() << ":" << endl;
        p->processPayment(99.99);
        p->refund(25.00);
        cout << endl;
    }
    
    return 0;
}

Task: Create abstract Database with connect(), disconnect(), query(string). Implement MySQL and SQLite. Create a DatabaseManager that works with any database type.

Show Solution
#include <iostream>
#include <string>
using namespace std;

class Database {
public:
    virtual bool connect() = 0;
    virtual void disconnect() = 0;
    virtual string query(string sql) = 0;
    virtual string getName() = 0;
    virtual ~Database() {}
};

class MySQL : public Database {
private:
    string host;
    bool connected = false;
public:
    MySQL(string h) : host(h) {}
    
    bool connect() override {
        cout << "Connecting to MySQL at " << host << endl;
        connected = true;
        return true;
    }
    
    void disconnect() override {
        cout << "Disconnecting from MySQL" << endl;
        connected = false;
    }
    
    string query(string sql) override {
        if (!connected) return "Error: Not connected";
        return "MySQL result for: " + sql;
    }
    
    string getName() override { return "MySQL"; }
};

class SQLite : public Database {
private:
    string filename;
    bool connected = false;
public:
    SQLite(string f) : filename(f) {}
    
    bool connect() override {
        cout << "Opening SQLite file: " << filename << endl;
        connected = true;
        return true;
    }
    
    void disconnect() override {
        cout << "Closing SQLite file" << endl;
        connected = false;
    }
    
    string query(string sql) override {
        if (!connected) return "Error: Not connected";
        return "SQLite result for: " + sql;
    }
    
    string getName() override { return "SQLite"; }
};

class DatabaseManager {
private:
    Database* db;
public:
    DatabaseManager(Database* database) : db(database) {}
    
    void initialize() {
        cout << "Initializing " << db->getName() << endl;
        db->connect();
    }
    
    void executeQuery(string sql) {
        cout << db->query(sql) << endl;
    }
    
    void shutdown() {
        db->disconnect();
    }
};

int main() {
    MySQL mysql("localhost:3306");
    SQLite sqlite("data.db");
    
    DatabaseManager manager1(&mysql);
    manager1.initialize();
    manager1.executeQuery("SELECT * FROM users");
    manager1.shutdown();
    
    cout << endl;
    
    DatabaseManager manager2(&sqlite);
    manager2.initialize();
    manager2.executeQuery("SELECT * FROM products");
    manager2.shutdown();
    
    return 0;
}

Task: Create a template class DataProcessor<T> with virtual process(T data). Create NumberProcessor and StringProcessor that specialize for int and string. Also overload a global analyze() function for both types.

Show Solution
#include <iostream>
#include <string>
#include <cctype>
using namespace std;

// Template base class (compile-time polymorphism)
template<typename T>
class DataProcessor {
public:
    virtual void process(T data) {
        cout << "Processing data: " << data << endl;
    }
    virtual ~DataProcessor() {}
};

// Specialization for int (runtime polymorphism)
class NumberProcessor : public DataProcessor<int> {
public:
    void process(int data) override {
        cout << "Number: " << data << " | ";
        cout << "Square: " << (data * data) << " | ";
        cout << "Even: " << (data % 2 == 0 ? "Yes" : "No") << endl;
    }
};

// Specialization for string (runtime polymorphism)
class StringProcessor : public DataProcessor<string> {
public:
    void process(string data) override {
        cout << "String: " << data << " | ";
        cout << "Length: " << data.length() << " | ";
        
        int upper = 0;
        for (char c : data) {
            if (isupper(c)) upper++;
        }
        cout << "Uppercase: " << upper << endl;
    }
};

// Function overloading (compile-time polymorphism)
void analyze(int value) {
    cout << "Analyzing integer: " << value;
    cout << " (Type: number)" << endl;
}

void analyze(string value) {
    cout << "Analyzing string: " << value;
    cout << " (Type: text)" << endl;
}

int main() {
    NumberProcessor numProc;
    StringProcessor strProc;
    
    // Using pointers for runtime polymorphism
    DataProcessor<int>* ptrNum = &numProc;
    DataProcessor<string>* ptrStr = &strProc;
    
    cout << "=== Runtime Polymorphism ===\n";
    ptrNum->process(42);
    ptrStr->process("HelloWorld");
    
    cout << "\n=== Compile-time Polymorphism ===\n";
    analyze(100);
    analyze("C++ Programming");
    
    return 0;
}
05

Types of Polymorphism

C++ supports two main types of polymorphism: compile-time (static) and runtime (dynamic). Understanding when to use each helps you write efficient, flexible code. Compile-time polymorphism is resolved by the compiler, while runtime polymorphism is resolved during program execution.

Compile-Time Polymorphism

Resolved at compile time. Faster execution, no runtime overhead. Includes:

  • Function Overloading: Same name, different parameters
  • Operator Overloading: Custom behavior for operators
  • Templates: Generic programming
Runtime Polymorphism

Resolved at runtime. More flexible, supports dynamic behavior. Includes:

  • Virtual Functions: Dynamic dispatch via vtable
  • Function Overriding: Derived class replaces base
  • Abstract Classes: Interface-based design
// Compile-time polymorphism: Function Overloading
class Calculator {
public:
    int add(int a, int b) {
        return a + b;
    }
    
    double add(double a, double b) {
        return a + b;
    }
    
    string add(string a, string b) {
        return a + b;
    }
};

int main() {
    Calculator calc;
    cout << calc.add(5, 3) << endl;           // 8 (int version)
    cout << calc.add(5.5, 3.2) << endl;       // 8.7 (double version)
    cout << calc.add("Hello", "World") << endl; // HelloWorld (string)
    return 0;
}

Function overloading is resolved at compile time based on the argument types. The compiler determines which add() version to call by examining the arguments. There is no runtime overhead - the decision is made before the program runs.

// Compile-time polymorphism: Templates
template<typename T>
T maximum(T a, T b) {
    return (a > b) ? a : b;
}

int main() {
    cout << maximum(10, 20) << endl;      // 20 (int)
    cout << maximum(5.5, 3.2) << endl;    // 5.5 (double)
    cout << maximum('a', 'z') << endl;    // z (char)
    return 0;
}

Templates are another form of compile-time polymorphism. The compiler generates specialized versions of the function for each type used. This is called "parametric polymorphism" and provides type safety with zero runtime cost.

// Runtime polymorphism: Virtual Functions
class Renderer {
public:
    virtual void render() = 0;
    virtual ~Renderer() {}
};

class OpenGLRenderer : public Renderer {
public:
    void render() override {
        cout << "Rendering with OpenGL" << endl;
    }
};

class VulkanRenderer : public Renderer {
public:
    void render() override {
        cout << "Rendering with Vulkan" << endl;
    }
};

void renderScene(Renderer* renderer) {
    renderer->render();  // Decided at runtime
}

int main() {
    OpenGLRenderer gl;
    VulkanRenderer vk;
    
    renderScene(&gl);  // Rendering with OpenGL
    renderScene(&vk);  // Rendering with Vulkan
    return 0;
}

Runtime polymorphism uses virtual functions and is resolved through vtable lookup during execution. This allows you to write code that works with objects whose exact type is not known until runtime - essential for plugin systems, frameworks, and flexible architectures.

Aspect Compile-Time Runtime
When Resolved During compilation During execution
Performance Faster (no overhead) Slight overhead (vtable)
Flexibility Less (types known at compile) More (types can vary)
Mechanisms Overloading, Templates Virtual functions, Override
Use Case Performance-critical code Extensible architectures
Virtual Destructor Rule: If a class has any virtual functions, always make the destructor virtual too. Otherwise, deleting a derived object through a base pointer causes undefined behavior - the derived destructor will not be called!
// Why virtual destructors matter
class Base {
public:
    ~Base() {  // Non-virtual - DANGEROUS!
        cout << "Base destructor" << endl;
    }
};

class Derived : public Base {
public:
    ~Derived() {
        cout << "Derived destructor" << endl;
    }
};

int main() {
    Base* ptr = new Derived();
    delete ptr;  // Only "Base destructor" called - memory leak!
    return 0;
}

// FIX: virtual ~Base() { ... }

Practice Questions: Types of Polymorphism

Task: Create a Printer class with overloaded print() methods for int, double, string, and arrays (int* with size).

Show Solution
#include <iostream>
#include <string>
using namespace std;

class Printer {
public:
    void print(int value) {
        cout << "Integer: " << value << endl;
    }
    
    void print(double value) {
        cout << "Double: " << value << endl;
    }
    
    void print(string value) {
        cout << "String: " << value << endl;
    }
    
    void print(int* arr, int size) {
        cout << "Array: [";
        for (int i = 0; i < size; i++) {
            cout << arr[i];
            if (i < size - 1) cout << ", ";
        }
        cout << "]" << endl;
    }
};

int main() {
    Printer p;
    
    p.print(42);
    p.print(3.14159);
    p.print("Hello World");
    
    int nums[] = {1, 2, 3, 4, 5};
    p.print(nums, 5);
    
    return 0;
}

Task: Create a template class Box<T> with set(T), get(), and isEmpty(). Test with int, string, and double.

Show Solution
#include <iostream>
#include <string>
using namespace std;

template<typename T>
class Box {
private:
    T content;
    bool hasContent = false;
    
public:
    void set(T value) {
        content = value;
        hasContent = true;
    }
    
    T get() {
        return content;
    }
    
    bool isEmpty() {
        return !hasContent;
    }
    
    void clear() {
        hasContent = false;
    }
};

int main() {
    Box<int> intBox;
    cout << "Int box empty: " << intBox.isEmpty() << endl;
    intBox.set(42);
    cout << "Int box value: " << intBox.get() << endl;
    
    Box<string> stringBox;
    stringBox.set("Hello Template!");
    cout << "String box value: " << stringBox.get() << endl;
    
    Box<double> doubleBox;
    doubleBox.set(3.14159);
    cout << "Double box value: " << doubleBox.get() << endl;
    
    return 0;
}

Task: Create a template class DataProcessor<T> with virtual process(T data). Create NumberProcessor and StringProcessor that specialize for int and string. Also overload a global analyze() function for both types.

Show Solution
#include <iostream>
#include <string>
#include <cctype>
using namespace std;

// Template base class (compile-time polymorphism)
template<typename T>
class DataProcessor {
public:
    virtual void process(T data) {
        cout << "Processing data: " << data << endl;
    }
    virtual ~DataProcessor() {}
};

// Specialization for int (runtime polymorphism)
class NumberProcessor : public DataProcessor<int> {
public:
    void process(int data) override {
        cout << "Number: " << data << " | ";
        cout << "Square: " << (data * data) << " | ";
        cout << "Even: " << (data % 2 == 0 ? "Yes" : "No") << endl;
    }
};

// Specialization for string (runtime polymorphism)
class StringProcessor : public DataProcessor<string> {
public:
    void process(string data) override {
        cout << "String: " << data << " | ";
        cout << "Length: " << data.length() << " | ";
        
        int upper = 0;
        for (char c : data) {
            if (isupper(c)) upper++;
        }
        cout << "Uppercase: " << upper << endl;
    }
};

// Function overloading (compile-time polymorphism)
void analyze(int value) {
    cout << "Analyzing integer: " << value;
    cout << " (Type: number)" << endl;
}

void analyze(string value) {
    cout << "Analyzing string: " << value;
    cout << " (Type: text)" << endl;
}

int main() {
    NumberProcessor numProc;
    StringProcessor strProc;
    
    // Using pointers for runtime polymorphism
    DataProcessor<int>* ptrNum = &numProc;
    DataProcessor<string>* ptrStr = &strProc;
    
    cout << "=== Runtime Polymorphism ===" << endl;
    ptrNum->process(42);
    ptrStr->process("HelloWorld");
    
    cout << "\n=== Compile-time Polymorphism ===" << endl;
    analyze(100);
    analyze("C++ Programming");
    
    return 0;
}

Task: Create a Vector2D class with operator overloading for +, -, and <<. Then create a virtual magnitude() method and derive Vector3D that overrides it.

Show Solution
#include <iostream>
#include <cmath>
using namespace std;

class Vector2D {
protected:
    double x, y;
public:
    Vector2D(double xVal = 0, double yVal = 0) : x(xVal), y(yVal) {}
    
    // Operator overloading (compile-time polymorphism)
    Vector2D operator+(const Vector2D& other) {
        return Vector2D(x + other.x, y + other.y);
    }
    
    Vector2D operator-(const Vector2D& other) {
        return Vector2D(x - other.x, y - other.y);
    }
    
    // Virtual function (runtime polymorphism)
    virtual double magnitude() {
        return sqrt(x * x + y * y);
    }
    
    virtual void display() {
        cout << "Vector2D(" << x << ", " << y << ")";
    }
    
    virtual ~Vector2D() {}
    
    friend ostream& operator<<(ostream& os, Vector2D& v) {
        os << "(" << v.x << ", " << v.y << ")";
        return os;
    }
};

class Vector3D : public Vector2D {
private:
    double z;
public:
    Vector3D(double xVal = 0, double yVal = 0, double zVal = 0) 
        : Vector2D(xVal, yVal), z(zVal) {}
    
    double magnitude() override {
        return sqrt(x * x + y * y + z * z);
    }
    
    void display() override {
        cout << "Vector3D(" << x << ", " << y << ", " << z << ")";
    }
};

int main() {
    Vector2D v1(3, 4);
    Vector2D v2(1, 2);
    
    // Compile-time polymorphism (operator overloading)
    Vector2D v3 = v1 + v2;
    cout << "v1 + v2 = " << v3 << endl;
    
    Vector2D v4 = v1 - v2;
    cout << "v1 - v2 = " << v4 << endl;
    
    // Runtime polymorphism (virtual functions)
    Vector3D v5(3, 4, 5);
    
    Vector2D* ptr1 = &v1;
    Vector2D* ptr2 = &v5;
    
    cout << "\nMagnitudes:" << endl;
    cout << "2D Vector: " << ptr1->magnitude() << endl;
    cout << "3D Vector: " << ptr2->magnitude() << endl;
    
    return 0;
}

Key Takeaways

Polymorphism Defined

Allows objects of different types to be treated through a common interface, with each responding appropriately

Virtual Functions

Use the virtual keyword to enable dynamic binding and runtime method selection

Override Keyword

Always use override to ensure you are correctly overriding a base class virtual function

Abstract Classes

Use pure virtual functions (= 0) to create abstract classes that define interfaces

Virtual Destructors

Always make destructors virtual in base classes to prevent memory leaks when deleting through base pointers

Choose Wisely

Use compile-time polymorphism for performance, runtime polymorphism for flexibility and extensibility

Knowledge Check

Test your understanding of C++ Polymorphism:

Question 1 of 6

What keyword enables runtime polymorphism in C++?

Question 2 of 6

What makes a class abstract in C++?

Question 3 of 6

Why should you use the override keyword when overriding a virtual function?

Question 4 of 6

Which type of polymorphism is function overloading?

Question 5 of 6

Why should base classes with virtual functions have virtual destructors?

Question 6 of 6

What happens when you try to instantiate an abstract class?

Answer all questions to check your score