Inheritance & Polymorphism

Reviewed & published by Brayan K

By the end of this lesson you'll be able to build a class hierarchy with base and derived classes, call base constructors correctly, override behaviour with virtual functions, and call one method on many object types through a base pointer — the heart of flexible, extensible C++.

Part of the free C++ course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

💡 Real-World Analogy

Think of inheritance like biology. Animal is the general category — every animal can eat and sleep. A Dog is an Animal, so it gets all those traits for free, and adds its own (barking). That "is-a" link is exactly what class Dog : public Animal means. Polymorphism is the next idea: if you ask a line-up of different animals to "speak", each makes its own sound — same instruction, different result. In C++ the virtual keyword is what makes that happen, deciding the right behaviour while the program runs.

🔐 Member Access Under Inheritance

AccessBase classDerived classOutside
public✅✅✅
protected✅✅❌
private✅❌❌

Use protected for data a derived class needs to touch but the outside world shouldn't. private base members still exist in the derived object — they're just only reachable through the base's own public/protected methods.

1. Base & Derived Classes

A base class holds what's shared; a derived class adds what's special. You write class Derived : public Base — the public keyword gives you public inheritance, the normal "Derived is-a Base" relationship. The derived object contains the base part, so it can use the base's public and protected members as if they were its own. Read this worked example and run it.

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

// BASE class — the shared, general thing.
class Animal {
protected:                 // 'protected' = visible to derived classes, hidden outside
    string name;
public:
    // Base constructor: sets up the part every Animal shares.
    Animal(string n) : name(n) {
        cout << "Animal ctor: " << name << endl;
    }
    void eat() const {
        cout << name << " is eating." << endl;
    }
};

// DERIVED class — 'public' inheritance means "Dog IS-A Animal".
class Dog : public Animal {
    string breed;
public:
    // The base part must be built first: call Animal(n) in the init list.
    Dog(string n, string b) : Animal(n), breed(b) {
        cout << "Dog ctor: " << breed << endl;
    }
    void bark() const {                 // Dog-specific behaviour
        cout << name << " says: Woof!" << endl;   // 'name' is inherited
    }
};

int main() {
    Dog rex("Rex", "Collie");  // Animal ctor runs first, then Dog ctor
    rex.eat();                 // inherited from Animal
    rex.bark();                // defined in Dog
    return 0;
}
// ✅ Expected output:
//    Animal ctor: Rex
//    Dog ctor: Collie
//    Rex is eating.
//    Rex says: Woof!

Notice the constructor: Dog(string n, string b) : Animal(n), breed(b). The base part must be built first, so you call the base constructor in the init list. If you don't, C++ tries to call Animal() with no arguments — and since Animal has no such constructor, that's a compile error. Now your turn: finish the Car class below.

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

class Vehicle {
protected:
    string brand;
public:
    Vehicle(string b) : brand(b) {}
    void honk() const { cout << brand << ": Beep beep!" << endl; }
};

// 🎯 YOUR TURN — make Car inherit from Vehicle.

// 1) Inherit publicly from Vehicle
class Car : public ___ {        // 👉 put the base class name here
    int doors;
public:
    // 2) Pass 'b' up to the Vehicle constructor, store 'd' in doors
    Car(string b, int d) : ___(b), doors(d) {}   // 👉 call Vehicle(b)

    void describe() const {
        cout << brand << " with " << doors << " doors" << endl;
    }
};

int main() {
    Car c("Toyota", 4);
    c.honk();        // inherited from Vehicle
    c.describe();    // defined in Car

    // ✅ Expected output:
    //    Toyota: Beep beep!
    //    Toyota with 4 doors
    return 0;
}

2. Virtual Functions, override & Polymorphism

Polymorphism means "many forms": you call one method through a base pointer and get the right derived behaviour. The magic word is virtual. Mark a base method virtual to say "a child may replace me", then in the child write the same method with override. Because the choice is made while the program runs, this is called runtime polymorphism (or dynamic dispatch). The key rule: it only works through a base pointer or reference, never a plain value.

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

class Animal {
protected:
    string name;
public:
    Animal(string n) : name(n) {}

    // 'virtual' lets a derived class REPLACE this through a base pointer.
    virtual void speak() const {
        cout << name << " makes a sound." << endl;
    }

    // VIRTUAL DESTRUCTOR — without it, 'delete base;' on a derived
    // object would not run the derived destructor. Always add it.
    virtual ~Animal() = default;
};

class Dog : public Animal {
public:
    Dog(string n) : Animal(n) {}
    // 'override' = "I am replacing a base virtual function".
    // The compiler errors if no matching base function exists.
    void speak() const override {
        cout << name << " says: Woof!" << endl;
    }
};

class Cat : public Animal {
public:
    Cat(string n) : Animal(n) {}
    void speak() const override {
        cout << name << " says: Meow!" << endl;
    }
};

int main() {
    // One base-pointer type holds many derived types.
    vector<Animal*> zoo;
    zoo.push_back(new Dog("Rex"));
    zoo.push_back(new Cat("Felix"));
    zoo.push_back(new Animal("Thing"));

    // Same call, different behaviour — chosen at RUNTIME.
    for (Animal* a : zoo) {
        a->speak();
    }
    // Output:
    //   Rex says: Woof!
    //   Felix says: Meow!
    //   Thing makes a sound.

    for (Animal* a : zoo) delete a;   // safe because ~Animal is virtual
    return 0;
}

// ✅ Expected output:
//    Rex says: Woof!
//    Felix says: Meow!
//    Thing makes a sound.

See how a->speak() runs Dog::speak for the dog and Cat::speak for the cat, even though a is an Animal*? That's dynamic dispatch. Now add the virtual and override keywords yourself:

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

class Employee {
protected:
    string name;
public:
    Employee(string n) : name(n) {}
    // 1) Make this overridable so derived classes can replace it
    ___ double pay() const {        // 👉 add the keyword that means "overridable"
        return 2000;                // base pay
    }
    virtual ~Employee() = default;
};

class Manager : public Employee {
public:
    Manager(string n) : Employee(n) {}
    // 2) Replace pay() and mark it correctly
    double pay() const ___ {        // 👉 the keyword that means "I replace a base virtual"
        return 5000;
    }
};

int main() {
    vector<Employee*> staff;
    staff.push_back(new Employee("Sam"));
    staff.push_back(new Manager("Alex"));

    for (Employee* e : staff)
        cout << "Pay: " << e->pay() << endl;

    // ✅ Expected output:
    //    Pay: 2000
    //    Pay: 5000
    for (Employee* e : staff) delete e;
    return 0;
}

3. Abstract Classes & Pure Virtual Functions

Sometimes the base class has no sensible default — what's the area of a generic "Shape"? You make the function pure virtual by writing = 0: virtual double area() const = 0;. That has two effects. First, every derived class must implement it. Second, the base becomes an abstract class — you can't create one directly, only its concrete children. Abstract classes are how C++ defines an interface.

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

// ABSTRACT class: it has a PURE virtual function ( = 0 ).
// You CANNOT create a Shape directly — only classes that
// provide every pure virtual can be instantiated.
class Shape {
public:
    // '= 0' makes this PURE virtual: derived classes MUST define it.
    virtual double area() const = 0;

    // A virtual destructor is still required for safe deletion.
    virtual ~Shape() = default;

    // A normal (non-pure) function the children can reuse.
    void report() const {
        cout << "Area = " << area() << endl;   // calls the derived area()
    }
};

class Square : public Shape {
    double side;
public:
    Square(double s) : side(s) {}
    double area() const override { return side * side; }
};

int main() {
    // Shape s;          // ❌ compile error: Shape is abstract
    Shape* s = new Square(4.0);   // ✅ base pointer to a concrete child
    s->report();                  // Area = 16
    delete s;
    return 0;
}

// ✅ Expected output:
//    Area = 16

Deep Dive: the Virtual Destructor Rule

When you delete an object through a base pointer, C++ needs to know which destructor to run. If the base destructor is not virtual, only the base part is destroyed — the derived part leaks, and the standard calls this undefined behaviour.

The rule is simple: any class with virtual functions should declare a virtual destructor. Often virtual ~Base() = default; is all you need.

class Base {
public:
    virtual ~Base() = default;   // ✅ makes 'delete base;' safe
};
class Derived : public Base { /* ... */ };

Base* p = new Derived();
delete p;   // runs ~Derived() then ~Base() — correct cleanup

Pro Tips

Common Errors (and the fix)

📋 Quick Reference

GoalCode
Inherit publiclyclass Dog : public Animal { };
Call base constructorDog(string n) : Animal(n) { }
Make overridablevirtual void speak() const;
Override in childvoid speak() const override;
Pure virtual (abstract)virtual double area() const = 0;
Virtual destructorvirtual ~Animal() = default;
Polymorphic callAnimal* a = new Dog(); a->speak();

Mini-Challenge: Shape Hierarchy

No blanks this time — just a brief and an outline. Build an abstract Shape with a pure virtual area(), derive Circle and Rectangle, then loop over a vector<Shape*> and print each area. Check your output against the comments.

#include <iostream>
using namespace std;

int main() {
    // 🎯 MINI-CHALLENGE: a tiny shape hierarchy
    //
    // 1. Make an abstract base class Shape with:
    //       virtual double area() const = 0;   // pure virtual
    //       virtual ~Shape() = default;        // virtual destructor
    //
    // 2. class Circle : public Shape — store a radius;
    //       area() returns 3.14159 * radius * radius
    //
    // 3. class Rectangle : public Shape — store width & height;
    //       area() returns width * height
    //
    // 4. In main, store both in a vector<Shape*>, loop with a base
    //    pointer, print each area(), then 'delete' each one.
    //
    // ✅ Expected (Circle r=2, Rectangle 3x4):
    //    12.5664
    //    12

    // your code here
    return 0;
}

🎉 Lesson Complete

Practice quiz

What does 'class Dog : public Animal' express?

  • Dog contains a copy of Animal's name
  • Animal inherits from Dog
  • A 'Dog is-a Animal' relationship (public inheritance)
  • Dog and Animal are unrelated

Answer: A 'Dog is-a Animal' relationship (public inheritance). Public inheritance models the 'is-a' relationship. The Dog object contains the Animal part and can use Animal's public/protected members.

How does a derived constructor build the base part of the object?

  • By calling the base constructor in the init list, e.g. Dog(string n) : Animal(n)
  • It cannot
  • By calling delete on the base
  • Automatically with no syntax ever needed if the base has arguments

Answer: By calling the base constructor in the init list, e.g. Dog(string n) : Animal(n). The base part must be built first, so you call the base constructor in the init list. If the base has no default constructor, omitting this is a compile error.

What does the 'virtual' keyword in a base class enable?

  • Faster compilation
  • It makes the function private
  • It prevents inheritance
  • A derived class can replace the function through a base pointer (runtime polymorphism)

Answer: A derived class can replace the function through a base pointer (runtime polymorphism). Marking a base method virtual says 'a child may replace me'. The right override is then chosen at runtime through a base pointer or reference.

Why is writing 'override' on a derived method recommended?

  • It makes the method faster
  • The compiler errors if no matching base virtual exists, catching signature typos
  • It is required for the code to compile
  • It deletes the base version

Answer: The compiler errors if no matching base virtual exists, catching signature typos. override is free insurance: a mismatched signature becomes a compile error instead of silently creating a brand-new function that never overrides.

Through what must you call a virtual function to get polymorphic behaviour?

  • A base pointer or reference
  • A base value (plain object)
  • A static_cast
  • A global variable

Answer: A base pointer or reference. Dynamic dispatch works only through a base pointer (Animal*) or reference (Animal&). Copying into a plain base value loses polymorphism.

What is object slicing?

  • Splitting a class into multiple files
  • Deleting half an array
  • Copying a derived object into a base VALUE, which drops the derived data and overrides
  • A template specialisation

Answer: Copying a derived object into a base VALUE, which drops the derived data and overrides. Base b = derived; copies only the base part. The derived data and overrides are 'sliced off', so virtual calls run the base version.

What makes a function pure virtual, and what is its effect on the class?

  • The 'final' keyword; it seals the class
  • Writing '= 0'; the class becomes abstract and cannot be instantiated
  • The 'static' keyword; it makes the class global
  • Writing 'const'; it makes the class read-only

Answer: Writing '= 0'; the class becomes abstract and cannot be instantiated. virtual double area() const = 0; is pure virtual. A class with at least one pure virtual is abstract — only derived classes that implement it can be instantiated.

Why should a polymorphic base class declare a virtual destructor?

  • For faster destruction
  • It is purely stylistic
  • To allow copying
  • So 'delete base;' on a derived object runs the derived destructor too

Answer: So 'delete base;' on a derived object runs the derived destructor too. Without a virtual destructor, deleting a derived object through a base pointer runs only ~Base() — the derived part leaks. This is undefined behaviour.

Given Animal* a = new Dog("Rex"); with virtual speak(), what does a->speak() do?

  • Calls Animal::speak
  • Calls Dog::speak (the override), chosen at runtime
  • Causes a compile error
  • Calls both versions

Answer: Calls Dog::speak (the override), chosen at runtime. Because speak is virtual and a points to a Dog, dynamic dispatch runs Dog::speak — 'Rex says: Woof!' — even though a is an Animal*.

Why shouldn't you rely on a virtual function called from a base constructor?

  • It is too slow
  • It always crashes
  • The derived part isn't built yet, so the BASE version runs, not the override
  • Constructors cannot call functions

Answer: The derived part isn't built yet, so the BASE version runs, not the override. During the base constructor the derived part doesn't exist yet, so C++ calls the base version of any virtual. The same applies in destructors.

Continue this course

Frequently asked questions

What is the difference between virtual and override in C++?

You write 'virtual' in the BASE class to say a function may be replaced by a derived class. You write 'override' in the DERIVED class to say this function replaces a base virtual. 'override' is optional but always recommended: the compiler checks a matching base virtual exists, so a typo in the signature becomes a compile error instead of a silently new function.

Why does my C++ class need a virtual destructor?

If you ever delete a derived object through a base-class pointer (Base* p = new Derived(); delete p;), the destructor must be virtual. Without it, only ~Base() runs and the derived part is never cleaned up — that is undefined behaviour and a common memory leak. Rule of thumb: any class with virtual functions should have a virtual destructor.

What is object slicing in C++?

Slicing happens when you copy a derived object into a base VALUE (Base b = derived;). Only the base part is copied; the derived data and overrides are 'sliced off', so virtual calls run the base version. To keep polymorphism, use a base pointer or reference (Base* or Base&), never a base value.

What is a pure virtual function and an abstract class?

A pure virtual function is declared with '= 0' and has no base implementation: virtual double area() const = 0;. A class containing at least one pure virtual is abstract — you cannot create an instance of it, only of derived classes that implement every pure virtual. Abstract classes are how C++ expresses interfaces.

Why shouldn't I call a virtual function in a constructor?

During a base constructor, the derived part of the object does not exist yet, so C++ calls the BASE version of any virtual — not the override you expected. The same applies in destructors. Avoid relying on virtual dispatch in constructors and destructors; do that work after the object is fully built.