C++ · Intermediate

Inheritance and Polymorphism in C++

12 min readUpdated September 24, 2026Every example verified

In short: Inheritance defines a class as an extension of a base class; polymorphism lets code holding a base-class pointer or reference call a virtual function and run the derived class's version chosen at run time. A pure virtual function makes the base abstract, override lets the compiler check each replacement, and a virtual destructor makes deleting through a base pointer safe.

One interface, several behaviours

Inheritance lets a class be defined as a variant of another. class BikeCourier : public Courier says a BikeCourier is a Courier: it has every member of Courier, may add its own, and may replace the behaviour of functions the base marked virtual. Code written against Courier then works, unchanged, with any courier invented later.

The replacement is called overriding, and it takes effect at run time only when the base function is virtual. A call such as c->fee(km) through a Courier* or Courier& looks at the actual type of the object and runs that class's version; this is polymorphism. Without virtual, the call is fixed at compile time from the pointer's type and the derived version is ignored. Mark each overriding function with override (C++11): the compiler then checks that a matching virtual function exists in the base, catching a misspelt name or wrong parameter list that would otherwise silently create a new, unrelated function.

Some base classes exist only to define an interface. Declaring a function pure virtual with = 0 says every concrete derived class must provide it; a class with any pure virtual function is abstract and cannot be instantiated, though pointers and references to it are fine. Members under protected: are hidden from outside code yet visible to derived classes, which is how DoorSensor reads id_. A derived constructor initialises its base part by naming the base in its initialiser list, TempSensor(...) : Sensor(id); construction runs base first, then derived, and destruction runs in reverse. A derived function can still call the base version explicitly, Delivery::fee(km).

Two rules prevent subtle bugs. Any class with virtual functions should have a virtual destructor; otherwise delete through a base pointer destroys only the base part, which is undefined behaviour. And polymorphism works only through pointers and references: passing a Siren to a function that takes Alarm by value copies just the Alarm part, an effect called slicing, and the copy behaves like a plain Alarm.

Syntax

 C++ · syntax
class Base {
public:
    Base(int s) : shared_(s) {}
    virtual void act() const;          // may be overridden
    virtual double cost() const = 0;   // pure virtual: Base is abstract
    virtual ~Base() = default;         // virtual destructor
protected:
    int shared_;                       // visible to derived classes only
};

class Derived : public Base {
public:
    Derived(int s) : Base(s) {}        // initialise the base part first
    void act() const override;         // override: checked by the compiler
    double cost() const override { return shared_ * 1.5; }
};

Base* p = new Derived(4);   // base pointer to a derived object
p->act();                   // runs Derived::act
delete p;                   // both destructors run because ~Base is virtual

public inheritance is the kind that models is-a and is what you want almost always. Adding final after a class name or a virtual function forbids further derivation or overriding.

Overriding a virtual function

Three courier types share one interface. The loop only knows about Courier pointers, yet each call picks the right fee formula. The input is a distance in kilometres.

 C++
#include <iostream>

class Courier {
public:
    virtual double fee(double km) const { return 3.0 + 1.0 * km; }
    virtual const char* name() const { return "standard"; }
    virtual ~Courier() = default;
};

class BikeCourier : public Courier {
public:
    double fee(double km) const override { return 2.0 + 0.5 * km; }
    const char* name() const override { return "bike"; }
};

class VanCourier : public Courier {
public:
    double fee(double km) const override { return 8.0 + 1.5 * km; }
    const char* name() const override { return "van"; }
};

int main() {
    double km;
    std::cin >> km;
    Courier standard;
    BikeCourier bike;
    VanCourier van;
    const Courier* options[] = {&standard, &bike, &van};
    for (const Courier* c : options) {
        std::cout << c->name() << ": " << c->fee(km) << "\n";
    }
    return 0;
}

Input given to the program: 4

Output

standard: 7
bike: 4
van: 14

options holds three const Courier* values, but the objects behind them are of three different classes. Because fee and name are virtual, c->fee(km) runs the version belonging to the real object, so the same loop prints three different formulas. override on each derived function makes the compiler verify that it really replaces a base function.

An abstract base with a pure virtual function

Sensor cannot be created on its own: reading is pure virtual. Each derived class supplies it and reuses the base's label helper and its protected id.

 C++
#include <iostream>
#include <string>

class Sensor {
public:
    Sensor(const std::string& id) : id_(id) {}
    virtual ~Sensor() = default;
    virtual std::string reading(int raw) const = 0;   // pure virtual: no body here
    std::string label() const { return "[" + id_ + "] "; }
protected:
    std::string id_;                                  // visible to derived classes
};

class TempSensor : public Sensor {
public:
    TempSensor(const std::string& id) : Sensor(id) {}
    std::string reading(int raw) const override {
        return label() + std::to_string(raw / 10) + "." + std::to_string(raw % 10) + " C";
    }
};

class DoorSensor : public Sensor {
public:
    DoorSensor(const std::string& id) : Sensor(id) {}
    std::string reading(int raw) const override {
        return label() + (raw == 0 ? "closed" : "open") + " (" + id_ + ")";
    }
};

void show(const Sensor& s, int raw) {      // accepts any kind of Sensor
    std::cout << s.reading(raw) << "\n";
}

int main() {
    TempSensor t("T1");
    DoorSensor d("D7");
    int a, b;
    std::cin >> a >> b;
    show(t, a);
    show(d, b);
    return 0;
}

Input given to the program: 215 1

Output

[T1] 21.5 C
[D7] open (D7)

show takes const Sensor& and works for any concrete sensor. Sensor s("X"); would not compile because the class is abstract, which is the intent: it exists to state what every sensor must provide. Both derived constructors pass the id up to Sensor(id), and DoorSensor reads id_ directly because it is protected, not private.

Slicing and the virtual destructor

The same Siren passed by value and by reference, then a Siren deleted through an Alarm pointer. Count how many destructor messages appear and in what order.

 C++
#include <iostream>

class Alarm {
public:
    virtual void ring() const { std::cout << "beep\n"; }
    virtual ~Alarm() { std::cout << "Alarm gone\n"; }
};

class Siren : public Alarm {
public:
    void ring() const override { std::cout << "WAIL\n"; }
    ~Siren() override { std::cout << "Siren gone\n"; }
};

void byValue(Alarm a) { a.ring(); }          // copies only the Alarm part
void byRef(const Alarm& a) { a.ring(); }     // keeps the real type

int main() {
    {
        Siren s;
        byValue(s);
        byRef(s);
    }
    std::cout << "--\n";
    Alarm* p = new Siren;
    delete p;                                // virtual destructor: both run
    return 0;
}

Output

beep
Alarm gone
WAIL
Siren gone
Alarm gone
--
Siren gone
Alarm gone

byValue(s) copies only the Alarm part of the siren, so the copy prints beep and, when it dies at the end of the function, only Alarm gone. byRef(s) keeps the real type and prints WAIL. When s leaves its block, the derived destructor runs first and then the base one. The final delete p also runs both, because ~Alarm is virtual; without that keyword only Alarm gone would appear and the behaviour would be undefined.

Three kinds of member function

Declared in Base asCall through Base* runsDerived must provide it?
void f();Base::f, alwaysno
virtual void f();the most derived overrideno, Base::f is the default
virtual void f() = 0;the override (Base itself cannot be created)yes, or it stays abstract

Common mistakes

  • Forgetting virtual on the base function

    Why it goes wrong: Without virtual, c->fee(km) through a Courier* always runs Courier::fee, no matter what object it points at.

    Fix: Declare the function virtual in the base and mark each replacement override.

  • Deleting through a base pointer without a virtual destructor

    Why it goes wrong: delete p where p is Alarm* runs only ~Alarm if it is not virtual; the derived part is never destroyed and the behaviour is undefined.

    Fix: Give every class with virtual functions a virtual destructor, even an empty one.

     C++ · fix
    virtual ~Alarm() = default;
  • Passing a derived object by value

    Why it goes wrong: A parameter of type Alarm receives a copy of only the Alarm part, so virtual calls inside the function use the base behaviour. This is slicing, and it compiles silently.

    Fix: Take polymorphic parameters as const Base& or Base*.

     C++ · fix
    void byRef(const Alarm& a) { a.ring(); }
  • Misspelling an override and omitting the override keyword

    Why it goes wrong: double Fee(double) const in a derived class is a brand-new function, not a replacement, so the base version keeps running and nothing warns you.

    Fix: Always write override; the compiler then rejects a function that overrides nothing.

Where you use this

Polymorphism is how a program stays open to new cases without editing old code. A report tool defines class Exporter with a pure virtual write, and adding a spreadsheet exporter next month means adding one class, not touching the loop that calls write on each exporter. Payment methods, notification channels, shapes in a drawing program and enemy behaviours in a game all follow the same shape: one abstract base naming the operations, several concrete classes, and code that holds base pointers or references.

The same mechanism makes testing easier: a fake Clock or fake Database derived from the interface can stand in for the real one. When the set of types is fixed and small, a template can often do the same job at compile time; virtual functions are the tool when the choice is made while the program runs.

 C++ · in practice
class Exporter {
public:
    virtual void write(const Report& r) = 0;
    virtual ~Exporter() = default;
};

void exportAll(const std::vector<Exporter*>& outs, const Report& r) {
    for (Exporter* e : outs) e->write(r);   // any exporter, present or future
}

Key points

  • class D : public B makes every D also a B, inheriting its members.
  • Only virtual functions are chosen at run time; mark each replacement override.
  • A pure virtual function (= 0) makes the class abstract: it defines an interface and cannot be instantiated.
  • Polymorphism needs a pointer or reference; passing by value slices the object.
  • Give a class with virtual functions a virtual destructor.
  • Construction runs base then derived; call the base constructor in the initialiser list.

Try it yourself

Express is meant to charge exactly double the standard fee, but it does not override fee yet, so both lines print the same number. Add the override to Express, calling Delivery::fee for the base amount, so the program prints standard 15 and express 30 for a distance of 10.

Your program
#include <iostream>

class Delivery {
public:
    virtual double fee(double km) const { return 5.0 + km; }
    virtual ~Delivery() = default;
};

class Express : public Delivery {
public:
    // add: double fee(double km) const override that returns twice Delivery::fee(km)
};

int main() {
    double km;
    std::cin >> km;
    Delivery standard;
    Express express;
    const Delivery* options[] = {&standard, &express};
    std::cout << "standard " << options[0]->fee(km) << "\n";
    std::cout << "express " << options[1]->fee(km) << "\n";
    return 0;
}
Input the program receives: 10
Expected output: standard 15 express 30

Practise this

Exercises for this lesson are in the C++ practice set.

Open the C++ playground

Frequently asked questions

What is the difference between overriding and overloading in C++?

Overloading is several functions with the same name but different parameter lists in the same scope, chosen at compile time from the arguments. Overriding is a derived class replacing a base class's virtual function with the same name and parameters, chosen at run time from the object's actual type.

When does a C++ class need a virtual destructor?

Whenever an object of a derived class might be deleted through a pointer to the base class, which is the normal situation for any class with virtual functions. Without it, only the base destructor runs and the behaviour is undefined. A class never used polymorphically, such as a plain struct, does not need one.

Progress is stored only in this browser.

How this page was checked. Every program on it was run with GCC 13 (C++17) at build time by the publishing checks, and the output shown is what it printed. Running C++ inside the browser is not available yet, so the Run button is absent rather than pretending; copy the code and run it with GCC 13 (C++17) locally.