Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Understanding Polymorphism in C++ Object-Oriented Programming

Tech 1

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:

  1. An inheritance relationship.
  2. The base class function must be declared virtual.
  3. The derived class must override the base class virtual function (matching function signature).
  4. 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.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.