Inheritance
Learning Objectives
Use single, multilevel, hierarchical and multiple inheritance.
Apply C++ public/protected/private inheritance and override/final.
Explain method hiding, the diamond problem and virtual inheritance.
Recognise the fragile base class problem and choose composition when appropriate.
Prerequisites: Modules 4–6.
Intuition and Definition
Inheritance defines a new class (derived, subclass, child) in terms of an existing one (base, superclass, parent): it inherits the base's members and can add or override behaviour. Used correctly it models an is-a relationship where a derived object can be used anywhere a base object is expected.
Car: Vehicle SportsCar: Car: Vehicle Car: Vehicle, Truck: Vehicle AmphibiousCar: Car, Boat
Combinations of the above (e.g. a diamond) C++ adds an inheritance access mode:
class D: ___ B B's public members become… B's protected members become… D *is a* B (the normal case)
D is *implemented in terms of* B (prefer composition)
Why It Matters
Inheritance gives polymorphic substitution plus reuse of base behaviour — but it is the tightest coupling in OOP: the derived class depends on the base's implementation details, not only its interface.
Syntax and Language Notes
class Vehicle { public: virtual int wheels() const { return 4; } virtual ~Vehicle() = default; };
class Motorbike final : public Vehicle { // final: no further derivation
public:
int wheels() const override { return 2; }
};// Java (not run): single class inheritance, multiple interfaces
public final class Motorbike extends Vehicle implements Insurable {
@Override public int wheels() { return 2; }
}public sealed class Motorbike : Vehicle, IInsurable
{
public override int Wheels() => 2; // base method must be virtual/abstract
}class Motorbike(Vehicle): # multiple inheritance allowed, resolved by MRO
def wheels(self):
return 2class Motorbike extends Vehicle { // single inheritance; mixins for more
override wheels(): number { return 2; }
}Complete Example
The fragile base class problem — a derived class that depends on how the base implements its methods:
#include <iostream>
#include <string>
struct Device {
Device() { std::cout << " Device()\n"; }
virtual ~Device() { std::cout << " ~Device()\n"; }
virtual std::string describe() const { return "device"; }
void log(int code) const { std::cout << " log(int " << code << ")\n"; }
void log(const std::string& s) const { std::cout << " log(string " << s << ")\n"; }
};
struct Ecu : Device {
Ecu() { std::cout << " Ecu()\n"; }
~Ecu() override { std::cout << " ~Ecu()\n"; }
std::string describe() const override { return "ECU, based on " + Device::describe(); }
void log(double v) const { std::cout << " log(double " << v << ")\n"; } // HIDES both base log()s
};
struct EngineEcu final : Ecu {
EngineEcu() { std::cout << " EngineEcu()\n"; }
~EngineEcu() override { std::cout << " ~EngineEcu()\n"; }
std::string describe() const override { return "engine " + Ecu::describe(); }
using Ecu::log; // brings Ecu::log(double) in; Device::log is still hidden by Ecu
};
// ---- the diamond ----
struct Powered { int id = 0; Powered() { std::cout << " Powered() constructed\n"; } };
struct Motor : virtual Powered {};
struct Genera
…construct:
Device()
Ecu()
EngineEcu()
describe: engine ECU, based on device
log(double 42)
log(int 42)
log(string via base reference)
destroy:
~EngineEcu()
~Ecu()
~Device()
diamond:
Powered() constructed
Motor view id=7, Generator view id=7#include <iostream>
#include <set>
#include <vector>
class IntSet {
std::set<int> items_;
public:
virtual ~IntSet() = default;
virtual void add(int x) { items_.insert(x); }
virtual void addAll(const std::vector<int>& xs) { for (int x : xs) add(x); } // calls add()!
size_t size() const { return items_.size(); }
};
class CountingSet : public IntSet { // wants to count how many adds were ATTEMPTED
int attempts_ = 0;
public:
void add(int x) override { ++attempts_; IntSet::add(x); }
void addAll(const std::vector<int>& xs) override { attempts_ += static_cast<int>(xs.size()); IntSet::addAll(xs); }
int attempts() const { return attempts_; }
};
// the composition alternative: wraps the set, does not depend on its internals
class CountingSetByComposition {
IntSet inner_;
int attempts_ = 0;
public:
void add(int x) { ++attempts_; inner_.add(x); }
void addAll(const std::vector<int>& xs) { attempts_ += static_cast<int>(xs.size()); inner_.addAll(xs); }
int attempts() const { return attempts_; }
};
int main() {
CountingSet s;
s.addAll({1, 2, 3});
std::cout << "inheritance: attempts = " << s.attempts() << " (expected 3)
…inheritance: attempts = 6 (expected 3)
composition: attempts = 3Step-by-Step Walkthrough
Construction runs Device() → Ecu() → EngineEcu(); destruction runs in reverse.
describe() overrides chain upward with qualified calls (Ecu::describe()).
Name hiding: declaring log(double) in Ecu hides *all* Device::log overloads in Ecu and below, so e.log(42) converts 42 to double. The qualified call e.Device::log(42) or a base reference still reaches them; using Device::log; inside Ecu would un-hide them.
Diamond: Motor and Generator both derive from Powered. With virtual inheritance there is a single Powered subobject — constructed once — so sg.id is unambiguous.
Fragile base: IntSet::addAll happens to call add(). CountingSet counts in both, so three additions are counted six times. Composition counts correctly because it does not depend on that internal detail.
classDiagram
Powered <|-- Motor : virtual
Powered <|-- Generator : virtual
Motor <|-- StarterGenerator
Generator <|-- StarterGeneratorCommon Mistakes
Inheriting to reuse code where there is no is-a relationship (Stack : public std::vector) — clients can now call insert in the middle of your stack.
Forgetting override, so a signature typo silently creates a new function.
Accidental hiding of base overloads by declaring one function with the same name.
Deep hierarchies (5+ levels) where behaviour is spread across many files.
Protected data members that every subclass mutates directly, freezing the base representation.
Trade-offs and Alternatives
Composition vs inheritance checklist — inherit only if all are true:
Derived *is a* base in every context (substitutable — LSP, Module 19).
Callers need to treat derived objects through the base interface.
The base class was designed for extension (documented hooks, virtual destructor).
You control both classes, or the base is a stable published abstraction.
Otherwise prefer composition: hold the other object as a member and delegate. Java and C# avoid multiple *class* inheritance; they combine single inheritance with multiple interfaces (and default methods), which avoids the diamond's state ambiguity but can still produce default-method conflicts that must be resolved explicitly.
Real-World Use
UI frameworks (Widget → Button), exception hierarchies (std::exception → std::runtime_error), and AUTOSAR-style driver layers use shallow, deliberate hierarchies. Most business logic uses composition plus interfaces.
Practice
Q1. (Easy) In what order do destructors run for C : B : A?
Answer: ~C, then ~B, then ~A.
Q2. (Medium) A derived class declares void draw(int); the base has virtual void draw() and void draw(double). What can you call on a derived object, and how do you restore the base overloads?
Answer: Only draw(int) is found by name lookup in the derived class (the others are hidden). Add using Base::draw; in the derived class.
Q3. (Advanced) Why does class Stack : public std::vector<int> violate good design, and what is the composition version?
Answer: Public inheritance exposes every vector operation (insert, erase, operator[]), so callers can break stack discipline, and std::vector has no virtual destructor. Composition keeps the vector private:
#include <iostream>
#include <stdexcept>
#include <vector>
class Stack {
std::vector<int> items_;
public:
void push(int x) { items_.push_back(x); }
int pop() {
if (items_.empty()) throw std::out_of_range("pop on empty stack");
int x = items_.back();
items_.pop_back();
return x;
}
bool empty() const { return items_.empty(); }
};
int main() {
Stack s;
s.push(1); s.push(2);
std::cout << s.pop() << s.pop() << "\n";
try { s.pop(); } catch (const std::exception& e) { std::cout << e.what() << "\n"; }
}21
pop on empty stackInterview Question
Q1. "What is the diamond problem and how do C++, Java and Python handle it?"
Answer: When D inherits from B and C, which both inherit from A, D could contain two A subobjects, and calls to A's members are ambiguous. C++ gives two copies by default and lets you declare virtual inheritance (B : virtual A) to share one; the most-derived class then constructs A. Java/C# forbid multiple class inheritance; interfaces carry no state, and conflicting default methods must be overridden explicitly (B.super.m()). Python allows multiple inheritance and resolves lookups with the C3 linearisation (MRO), so A appears once and super() follows the MRO.
Quick Quiz
Q1. What does final on a class mean?
Answer: No class may derive from it.
Q2. With virtual inheritance, who constructs the shared virtual base?
Answer: The most-derived class.
Q3. What is the fragile base class problem?
Answer: Changes to (or hidden behaviour of) a base class's implementation break derived classes that relied on it, even though the base's interface did not change.
Summary
Inheritance models substitutable is-a relationships; it is the strongest form of coupling.
Use override, final, virtual destructors; watch for name hiding.
Virtual inheritance solves the C++ diamond; Java/C# use interfaces; Python uses the MRO.
Prefer composition unless the checklist says inherit.
Related: Module 19 (LSP), Module 56 (advanced dispatch), Module 80 (Python MRO).