C++
Inheritance & Polymorphism
Inheritance lets one class be built directly on top of another, reusing its data and behavior instead of duplicating it. Polymorphism is what makes that genuinely useful in practice: code written against the general, base version can automatically run the correct, more specific behavior for whatever actual type it's handed — without ever needing to know which one it is.
- What it is
- Inheritance: a class ("derived") built on top of another ("base"), automatically getting its members. Polymorphism: calling a method through a base-class reference and having the correct derived-class version actually run.
- Mental model
- Inheritance is "a Car is a Vehicle" — every car has everything a vehicle has, plus more. Polymorphism is being handed a box labeled "Vehicle" and having it behave correctly whether the actual thing inside is a Car or a Truck, without you checking which.
- Key numbers
- A method must be declared
virtualin the base class for polymorphism to work — without it, C++ silently calls the base version instead of the derived one, a very common beginner surprise. - Reach for it when
- Several related types share common data/behavior but each also needs its own distinct version of specific behavior — a family of related shapes, sensors, or UI widgets is the classic case.
Inheritance, in a worked example
class Sensor {
public:
Sensor(const char *name) : name(name) {}
void printName() { std::cout << name << std::endl; }
protected:
const char *name; // "protected" — accessible to Sensor AND anything derived from it
};
class TemperatureSensor : public Sensor { // TemperatureSensor IS-A Sensor
public:
TemperatureSensor() : Sensor("Temperature") {} // calls the base constructor
float readCelsius() { return 23.5f; } // NEW behavior, specific to this sensor
};
TemperatureSensor t;
t.printName(); // inherited from Sensor, unchanged
t.readCelsius(); // TemperatureSensor's own, new method
TemperatureSensor automatically has everything Sensor has (name,
printName()) without rewriting either, plus its own additional method. This is the direct
reuse benefit of inheritance — the same relationship as a more specific
C struct that happens to start with the exact same first
fields as a more general one, except the compiler tracks and enforces the relationship for you.
virtual functions and polymorphism
Without virtual, calling a method through a base-class pointer always runs the base
class's version, even if the actual object is a derived type — often not what you want:
class Shape {
public:
virtual float area() { return 0; } // virtual — enables polymorphism
};
class Circle : public Shape {
public:
Circle(float r) : radius(r) {}
float area() override { return 3.14159f * radius * radius; } // overrides Shape's version
private:
float radius;
};
class Square : public Shape {
public:
Square(float s) : side(s) {}
float area() override { return side * side; }
private:
float side;
};
Shape *shapes[2] = { new Circle(5), new Square(4) };
for (int i = 0; i < 2; i++) {
std::cout << shapes[i]->area() << std::endl; // calls the RIGHT version for each, automatically
}
// 78.5397 (Circle), 16 (Square)
Every element of shapes is declared as a plain Shape*, but
area() still runs each one's own real, specific implementation — that dispatch, decided at
runtime based on the object's actual type, is exactly what "polymorphism" means.
The hierarchy, visually
area() on a Shape* pointing at a Circle runs Circle's version — the base class only defines the shared contract, not the actual behavior.Pure virtual functions and abstract classes
Shape::area() above had a (fairly meaningless) default implementation returning
0. A pure virtual function (= 0) instead says "every derived
class must provide its own implementation — there is no sensible default":
class Shape {
public:
virtual float area() = 0; // pure virtual — no implementation at all
};
// Shape shape; // COMPILE ERROR — Shape is now "abstract," cannot be instantiated directly
Circle c(5); // fine — Circle provides area(), so it CAN be instantiated
A class with at least one pure virtual function is an abstract class — it exists purely to define a shared interface that concrete derived classes must fill in, and can never be instantiated on its own.
Real-world applications
- Sensor driver interfaces — an abstract
Sensorbase class with a pure virtualread(), implemented differently byTemperatureSensor,PressureSensor, etc., letting the rest of a program treat every sensor uniformly. - GUI widget hierarchies — a base
Widgetclass with a virtualdraw(), overridden byButton,Checkbox, and every other concrete widget type. - Game entity systems — a base
Enemyclass with virtualattack()/update(), overridden per enemy type, so the game loop can treat an entire mixed army uniformly.
Gotchas
- Forgetting
virtualon the base class method is the single most common polymorphism bug — the code compiles fine and silently always calls the base version, never the derived override, which is easy to miss since there's no error. - A base class with any
virtualfunction should almost always also have avirtualdestructor — without one, deleting a derived object through a base-class pointer skips the derived class's own destructor, a real resource leak. - Deep inheritance hierarchies (many levels of "derives from") get hard to reason about fast — favor a small number of levels, and consider whether composition ("has-a") fits better than inheritance ("is-a") before adding another layer.
- The
overridekeyword (as used above) isn't required by the language, but it's cheap insurance — if the base method's signature doesn't actually match,overrideturns a silent "this doesn't override anything" mistake into a compile error.