C++ · Intermediate
Inheritance and Polymorphism in C++
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
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 virtualpublic 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.
#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.
#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.
#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 as | Call through Base* runs | Derived must provide it? |
|---|---|---|
| void f(); | Base::f, always | no |
| virtual void f(); | the most derived override | no, 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 aCourier*always runsCourier::fee, no matter what object it points at.Fix: Declare the function
virtualin the base and mark each replacementoverride.Deleting through a base pointer without a virtual destructor
Why it goes wrong:
delete pwherepisAlarm*runs only~Alarmif 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++ · fixvirtual ~Alarm() = default;Passing a derived object by value
Why it goes wrong: A parameter of type
Alarmreceives a copy of only theAlarmpart, so virtual calls inside the function use the base behaviour. This is slicing, and it compiles silently.Fix: Take polymorphic parameters as
const Base&orBase*.C++ · fixvoid byRef(const Alarm& a) { a.ring(); }Misspelling an override and omitting the override keyword
Why it goes wrong:
double Fee(double) constin 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.
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 Bmakes everyDalso aB, inheriting its members.- Only
virtualfunctions are chosen at run time; mark each replacementoverride. - 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.
#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;
}10standard 15
express 30#include <iostream>
class Delivery {
public:
virtual double fee(double km) const { return 5.0 + km; }
virtual ~Delivery() = default;
};
class Express : public Delivery {
public:
double fee(double km) const override { return 2 * 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;
}Practise this
Exercises for this lesson are in the C++ practice set.
Related lessons
- Pointers and References in C++What C++ pointers and references are, how & and * work, when nullptr is useful, and how to let a function change the variable it was given.10 min
- Templates in C++How C++ templates generate a function or class for each type you use, how type deduction works, and what C++17 adds with if constexpr and deduction guides.11 min
- Vectors in C++ (std::vector)How std::vector works in C++: creating and growing a vector, indexing with [] and at(), passing vectors to functions, erasing elements and building 2D grids.11 min
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.
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.