Notifications

No notifications

/Phase 3

Inheritance & Polymorphism

Reusing & Specialising Classes 🧬

Inheritance lets a class extend another. Polymorphism lets you call the right behaviour through a base-class pointer/reference.

class Animal {
public:
    virtual void speak() const { cout << "..."; }   // virtual = overridable
    virtual ~Animal() = default;                     // virtual destructor!
};

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

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

void greet(const Animal& a) { a.speak(); } // polymorphic call

int main() { Dog d; Cat c; greet(d); // Woof! greet(c); // Meow. }

The Three Ingredients

NeedKeyword
Mark a method as overridablevirtual
Confirm you're overridingoverride
Force derived classes to provide= 0 (pure virtual)
Forbid further overridingfinal

Inheritance Visibility

class Dog : public  Animal { /* ... */ };  // is-a (most common)
class Box : private Stack { /* ... */ };   // implemented-in-terms-of (rare)

Abstract Classes

A class with at least one pure virtual method:

class Shape {
public:
    virtual double area() const = 0;        // must be overridden
    virtual ~Shape() = default;
};
// Shape s;     // ❌ can't instantiate abstract class

On this page

Detailed Theory

Inheritance is C++'s mechanism for is-a relationships and code reuse. Polymorphism is what makes it powerful — the same call site dispatches to different code depending on the actual object.

Single Inheritance

class Vehicle {
protected:
    int wheels;
public:
    Vehicle(int w) : wheels(w) {}
    int getWheels() const { return wheels; }
};

class Car : public Vehicle { string brand; public: Car(string b) : Vehicle(4), brand(std::move(b)) {} const string& getBrand() const { return brand; } };

Car automatically has wheels and getWheels() because it derives from Vehicle.

Access Specifiers in Inheritance

Inherit aspublic membersprotected membersprivate members
publicstay publicstay protectedinaccessible
protectedbecome protectedstay protectedinaccessible
privatebecome privatebecome privateinaccessible

99% of the time you want public inheritance — that's the is-a relationship.

Constructor & Destructor Order

Constructor: Base then Derived
Destructor:  Derived then Base

You must initialise the base class explicitly when its constructor takes args:

class Dog : public Animal {
public:
    Dog(string name) : Animal(std::move(name)) {}    // call base ctor
};

Virtual Functions — Runtime Polymorphism

Without virtual, calling a method through a base reference uses the base version:

struct Base { void hello() { cout << "Base"; } };
struct Derived : Base { void hello() { cout << "Derived"; } };

Derived d; Base& r = d; r.hello(); // prints "Base" — static dispatch

With virtual, the actual object's method runs:

struct Base    { virtual void hello() { cout << "Base"; } };
struct Derived : Base { void hello() override { cout << "Derived"; } };

Derived d; Base& r = d; r.hello(); // prints "Derived" — dynamic dispatch

This is runtime polymorphism, the engine of OOP design patterns (Strategy, Observer, Visitor, …).

How It Works (sketch)

Each polymorphic class has a hidden pointer to a vtable — a function pointer table. r.hello() looks up hello in the vtable. Cost: one extra indirection.

override and final

class Dog : public Animal {
public:
    void speak() const override { cout << "Woof"; }   // compile-time check
    void run()  final           { /* can't be overridden further */ }
};

  • override tells the compiler "this is meant to override a base virtual" — typo? compile error.
  • final forbids further overriding (or, on a class, forbids derivation).
Always use override on overrides. Catches bugs.

Pure Virtual & Abstract Classes

class Shape {
public:
    virtual double area()      const = 0;
    virtual double perimeter() const = 0;
    virtual ~Shape()                  = default;
};

= 0 makes a method pure virtual. Any class with at least one pure virtual is abstract — you can't instantiate it; you can only derive and implement.

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

> Interfaces in C++ are just abstract classes with all-pure-virtual methods. No special keyword needed.

⚠️ Virtual Destructor — Critical

If you ever delete a derived object through a base pointer, the base's destructor MUST be virtual — otherwise only the base destructor runs, leaking the derived part.

class Animal { public: virtual ~Animal() = default; };  // ✅
class Dog    : public Animal { vector<string> tricks; };

Animal* a = new Dog{}; delete a; // both ~Dog (cleans tricks) and ~Animal run — correct

Rule: any class meant to be inherited from publicly should have a virtual destructor.

The Slicing Problem

Pass a derived object by value to a base parameter and the derived part is *sliced* off:

void describe(Animal a) { a.speak(); }   // by VALUE — slices

Dog d; describe(d); // calls Animal::speak — only the Animal sub-object survived

Always pass polymorphic types by reference or pointer (const Animal&, Animal*).

Calling the Base Version

class Logger : public Component {
public:
    void update() override {
        Component::update();        // call base implementation first
        log("updated");
    }
};

Multiple Inheritance — Use Sparingly

C++ allows it; mostly you'll see it for interfaces (mixing in pure-virtual classes):

class Printable    { public: virtual void print() const = 0; virtual ~Printable() = default; };
class Serialisable { public: virtual string toJson() const = 0; virtual ~Serialisable() = default; };

class Profile : public Printable, public Serialisable { /* ... */ };

Inheriting from multiple non-interface classes can lead to the diamond problem — solvable with virtual inheritance, but the pain often outweighs the gain. Prefer composition.

Composition vs Inheritance

> Prefer composition (has-a) over inheritance (is-a) unless you genuinely model an is-a relationship.

// Inheritance — Car IS-A Vehicle
class Car : public Vehicle { /* ... */ };

// Composition — Car HAS-A Engine class Car { Engine engine; // owns an engine Wheels wheels[4]; };

Cheat-Sheet

NeedCode
Inherit publiclyclass Dog : public Animal { ... };
Initialise baseDog() : Animal("dog") {}
Overridable methodvirtual void f();
Override in derivedvoid f() override;
Force impl in derivedvirtual void f() = 0; (pure virtual)
Lock further overridevoid f() final;
Polymorphic deletevirtual ~Base() = default;
Avoid slicingpass const T& or T*

You now have the OOP toolkit. Up next: memory management & smart pointers — how to own those polymorphic objects safely.