Skip to content

Inheritance & Polymorphism

Inheritance and polymorphism are the two pillars of object-oriented code reuse and abstraction. Inheritance allows a new class to inherit attributes and behaviors from an existing class, while polymorphism enables treated objects of different types to be interacted with through a unified interface.


1. Inheritance Basics

In C++, you inherit from a base class using a colon syntax. The inheritance mode (public, protected, or private) determines how the inherited members are exposed to the outside world.

#include <iostream>
#include <string>

class Device {
protected:
    std::string name;
public:
    Device(const std::string& n) : name(n) {}
    void power_on() { std::cout << name << " powering on.\n"; }
};

// Publicly inherits from Device
class Camera : public Device {
    int resolution;
public:
    Camera(const std::string& n, int res) : Device(n), resolution(res) {}
    void capture() { std::cout << "Capturing at " << resolution << "MP.\n"; }
};

Access Control in Inheritance

Base Member Access public Inheritance protected Inheritance private Inheritance
public Remains public Becomes protected Becomes private
protected Remains protected Becomes protected Becomes private
private Hidden (accessible only via base class functions) Hidden Hidden
  • public inheritance: Models an "IS-A" relationship (e.g. a Camera is a Device). This is by far the most common type of inheritance.
  • private / protected inheritance: Models "implemented in terms of." It is rarely used, as composition (having a member variable of the class type) is usually preferred.

2. Polymorphism and Virtual Functions

Polymorphism ("many forms") allows a base class pointer or reference to call member functions of a derived class at runtime. This dynamic behavior is triggered by marking functions with the virtual keyword.

#include <iostream>

class Shape {
public:
    virtual void draw() const {
        std::cout << "Drawing generic shape.\n";
    }
    virtual ~Shape() = default;
};

class Circle : public Shape {
public:
    // C++11 override: guarantees a base method signature is matched
    void draw() const override {
        std::cout << "Drawing a circle.\n";
    }
};

void render(const Shape& shape) {
    shape.draw(); // Resolves at runtime to call the correct function
}

The Under the Hood Mechanism: Vtables and Vptrs

Dynamic polymorphism is implemented using a Virtual Table (vtable). - Every class that declares or inherits a virtual function has a hidden vtable, which is an array of function pointers pointing to the virtual methods. - Every object instance of that class contains a hidden pointer called vptr (virtual pointer), which points to the class's vtable.

1
2
3
4
5
6
       Instance of Circle:
       ┌──────────────┐
       │   vptr       │ ───────&gt; [ Class Circle Vtable ]
       ├──────────────┤          ┌──────────────────────────┐
       │  (data members)│          │ Circle::draw() pointer   │
       └──────────────┘          └──────────────────────────┘

When calling shape.draw(), the program dereferences the object's vptr, locates the draw() entry in the vtable, and jumps to that function pointer. This adds a slight pointer-chasing overhead, which is the trade-off for dynamic flexibility.


3. Abstract Classes and Pure Virtual Functions

An Abstract Class defines an interface (contract) that derived classes must implement. You cannot instantiate an abstract class directly.

A function is declared pure virtual by appending = 0 to its declaration.

// Abstract Base Class
class Sensor {
public:
    virtual ~Sensor() = default; // Mandatory virtual destructor

    // Pure virtual function
    virtual double read() = 0; 
};

class TemperatureSensor : public Sensor {
public:
    double read() override {
        return 24.3; // Must implement to allow instantiation
    }
};

4. The Critical Rule of Virtual Destructors

If a class has at least one virtual function, its destructor must be declared virtual.

If you delete a derived object using a pointer to its base class, and the base class has a non-virtual destructor, only the base class destructor is executed. The derived class's destructor is bypassed, leading to Undefined Behavior and major memory/resource leaks.

#include <iostream>

class BadBase {
public:
    ~BadBase() { std::cout << "BadBase destroyed\n"; }
};

class Derived : public BadBase {
    int* buffer;
public:
    Derived() : buffer(new int[100]) {}
    ~Derived() { 
        delete[] buffer; 
        std::cout << "Derived destroyed\n"; 
    }
};

int main() {
    BadBase* ptr = new Derived();
    delete ptr; // LEAK! Only prints "BadBase destroyed", Derived destructor never runs.
}

The Fix:

1
2
3
4
class GoodBase {
public:
    virtual ~GoodBase() { std::cout << "GoodBase destroyed\n"; }
};

5. Controlling Overriding: final

You can restrict inheritance and overrides using the final keyword: - Final Class: Prevents other classes from inheriting from it. - Final Function: Prevents derived classes from overriding that specific virtual function further down the hierarchy.

1
2
3
4
5
6
7
8
9
class Controller final { /* Cannot be inherited */ };

class Base {
    virtual void setup();
};

class Intermediate : public Base {
    void setup() override final; // Child classes cannot override this
};

6. Casting in Inherited Hierarchies

C++ provides explicit cast operators to traverse class hierarchies safely.

  • static_cast: Performs fast compile-time casts up or down a hierarchy. It does not check types at runtime. Using it to downcast to an incorrect derived class leads to undefined behavior.
  • dynamic_cast: Performs safe runtime checks (downcasting). It checks the runtime type information (RTTI) of the object.
    • If casting a pointer fails, it returns nullptr.
    • If casting a reference fails, it throws a std::bad_cast exception.
    • Note: dynamic_cast only works on polymorphic classes (classes containing at least one virtual function).
#include <iostream>

class Animal { public: virtual ~Animal() = default; };
class Dog : public Animal { public: void bark() {} };
class Cat : public Animal { public: void meow() {} };

void play_with_animal(Animal* animal) {
    // Safely verify if the Animal is actually a Dog
    if (Dog* dog = dynamic_cast<Dog*>(animal)) {
        dog->bark();
    } else {
        std::cout << "Not a dog!\n";
    }
}

  • Classes and Objects — Encapsulation, constructors, and access specifiers.
  • Templates — Compile-time polymorphism vs runtime polymorphism.