SECTION 1
Historical Context & Motivation
Before object-oriented programming became mainstream, software engineers routinely duplicated code across modules that shared overlapping behavior. If a payroll system needed both HourlyEmployee and SalariedEmployee structures, every common field — name, ID, tax bracket — had to be copied, maintained, and debugged in two places. Inheritance arose as the central mechanism for eliminating this redundancy by letting one class automatically acquire the members of another. The concept did not appear overnight; it grew through decades of language experimentation, each milestone sharpening the notion that types can form hierarchies reflecting real-world is-a relationships.
1967 • Simula 67 Introduces Classes & Subclasses
1979 • Bjarne Stroustrup Begins 'C with Classes'
1985 • The C++ Programming Language, 1st Edition
1994 • The Liskov Substitution Principle (LSP) Formalized
2011–2020 • Modern C++ Refinements
The central question inheritance answers is deceptively simple: How do we express that one type is a specialized version of another without duplicating the general behavior? By framing this as an is-a relationship — a Dog is-a Animal, a Circle is-a Shape — C++ gives us a type-safe, zero-overhead mechanism that has shaped software architecture for nearly four decades.
SECTION 2
Core Principles & Definitions
Inheritance in C++ establishes a hierarchical relationship between a base class (also called a superclass or parent class) and one or more derived classes (subclasses or child classes). The derived class inherits the data members and member functions of the base class and may extend or override them. The following foundational ideas govern how and when to use this mechanism correctly.
Is-A Relationship
Code Reuse via Generalization
Access Control: public, protected, private
The Liskov Substitution Principle
Is-A vs. Has-A
KEY TAKEAWAY
SECTION 3
Visual Explanation — Inheritance Hierarchy
color member and a public area() function. Circle and Rectangle each publicly inherit from Shape, adding their own data members and overriding area(). The dashed box at the bottom highlights that both derived classes pass the is-a test.In the hierarchy above, the arrows point from the derived class toward the base class, following the standard UML convention for generalization. Notice that color is declared protected so that derived classes can access it directly, while external code cannot. The area() method is declared virtual in the base so that each derived class can supply its own formula — a concept we will explore further in the mechanism section. The critical design observation is that any function accepting a Shape& or Shape* can operate on a Circle or Rectangle without modification, because both satisfy the is-a contract.
SECTION 4
How C++ Inheritance Works Under the Hood
When the compiler encounters a derived class declaration such as class Circle : public Shape { ... };, it performs several concrete steps that determine the memory layout, access rules, and dispatch behavior of the resulting type. Understanding these steps is essential for reasoning about performance and correctness in non-trivial hierarchies.
Memory Layout: Base Sub-Object
A Circle object physically contains a base sub-object — the full memory footprint of a Shape — followed by its own additional data members. The compiler lays out the base members first so that a pointer or reference to Shape can point to the beginning of a Circle without an offset adjustment. This layout guarantee is what makes implicit upcasting — converting a Circle* to a Shape* — a zero-cost operation at runtime.
Construction Order
When a Circle is constructed, C++ guarantees that the base-class constructor runs first, initializing the Shape sub-object, before the Circle-specific initialization takes place. The derived constructor passes arguments to the base constructor using the member initializer list. If no base constructor is explicitly invoked, the compiler calls the base's default constructor. Destruction occurs in reverse order: the Circle destructor runs first, then the Shape destructor.
Access Specifiers in Inheritance
| Inheritance Mode | Base public members become | Base protected members become | Base private members |
|---|---|---|---|
public | public in Derived | protected in Derived | inaccessible |
protected | protected in Derived | protected in Derived | inaccessible |
private | private in Derived | private in Derived | inaccessible |
How the inheritance access specifier transforms base-class member visibility in the derived class.
Why private members are inaccessible
Virtual Functions and Dynamic Dispatch
Marking a base-class function as virtual enables dynamic dispatch: when calling area() through a Shape*, the runtime selects the derived class's version based on the actual object type, not the pointer type. The compiler typically implements this via a vtable — a per-class table of function pointers — and a hidden vptr stored in each object. The overhead is a single pointer per polymorphic object and one indirect function call per virtual invocation — negligible for most applications.
SECTION 5
Syntax, Structure, and the Is-A Litmus Test
The syntactic form for C++ inheritance is straightforward. You declare the derived class name, a colon, the access specifier, and the base class name. What is far more challenging is deciding when inheritance is appropriate. The diagram below presents a decision flowchart that codifies the is-a litmus test along with common pitfalls.
The canonical syntax for public inheritance in C++ is as follows:
C++ Syntax Template
class Derived : public Base { /* ... */ }; — The keyword public before Base means this is modeling an is-a relationship. Replace public with private or protected only when modeling implementation-of relationships, not is-a.- Step 1: Identify the general concept (Shape, Animal, Account) and define it as the base class with shared data and behavior.
- Step 2: Apply the is-a litmus test: does every instance of the derived class genuinely qualify as an instance of the base?
- Step 3: Declare the derived class using public inheritance, adding specialized members and overriding virtual functions as needed.
- Step 4: Initialize the base sub-object via the member initializer list in the derived constructor.
SECTION 6
Worked Example — Building a Shape Hierarchy
Let us build a complete, compilable example that demonstrates public inheritance, constructor delegation to the base, virtual function overriding, and polymorphic usage through a base-class pointer.
Shape Hierarchy with Polymorphic area()
- 1
Step 1 — Define the Base Class (Shape)
We defineShapewith aprotectedstring membercolor, a constructor, a virtualarea()function, and avirtualdestructor (essential for polymorphic deletion).class Shape { protected: std::string color; public: Shape(const std::string& c) : color(c) {} virtual double area() const { return 0.0; } std::string getColor() const { return color; } virtual ~Shape() = default; };Base classShapedefined with virtualarea()and virtual destructor. - 2
Step 2 — Derive Circle (is-a Shape)
Circle publicly inherits from Shape, adds aradiusdata member, and overridesarea(). Theoverridespecifier (C++11) asks the compiler to verify the function signature matches a base virtual.class Circle : public Shape { double radius; public: Circle(double r, const std::string& c) : Shape(c), radius(r) {} // delegate to base ctor double area() const override { return 3.14159265358979 * radius * radius; } };Circle inheritscolorandgetColor()from Shape and overridesarea()to return π × r². - 3
Step 3 — Derive Rectangle (is-a Shape)
Rectangle follows the same pattern, adding width and height.class Rectangle : public Shape { double width, height; public: Rectangle(double w, double h, const std::string& c) : Shape(c), width(w), height(h) {} double area() const override { return width * height; } };Rectangle overridesarea()to return width × height. - 4
Step 4 — Use Polymorphically Through Base Pointers
We store both derived objects in avector<Shape*>and iterate, callingarea(). Dynamic dispatch selects the correct override at runtime.#include <iostream> #include <vector> int main() { Circle c(5.0, "red"); Rectangle r(4.0, 6.0, "blue"); std::vector<Shape*> shapes = {&c, &r}; for (const Shape* s : shapes) { std::cout << s->getColor() << " shape, area = " << s->area() << '\n'; } }Output: red shape, area = 78.5398 | blue shape, area = 24 - 5
Step 5 — Verify the Is-A Guarantee
The loop body never mentions Circle or Rectangle — it works entirely throughShape*. This is the practical payoff of the is-a relationship: client code depends only on the base interface, remaining open to extension (add a Triangle tomorrow) and closed to modification (the loop needs no changes).The Open/Closed Principle is satisfied: new shapes require no changes to existing client code.
SECTION 7
Inheritance vs. Composition — Strengths & Limitations
A common software-engineering maxim is "Prefer composition over inheritance." This guidance does not condemn inheritance; it cautions against using it for relationships that are not genuinely is-a. The following table compares the two mechanisms along several design axes.
| Design Axis | Inheritance (is-a) | Composition (has-a) |
|---|---|---|
| Coupling | Tight — derived class depends on base-class implementation details. | Loose — composed object interacts only through the component's public interface. |
| Polymorphism | Built-in via virtual functions; derived objects substitutable for base. | Not automatic; requires explicit forwarding or the Strategy pattern. |
| Flexibility at runtime | Fixed at compile time — the base class cannot be changed after object creation. | High — composed members can be swapped at runtime via pointers/references. |
| Code reuse | Automatic — all non-private members visible in derived class. | Explicit — must forward or delegate calls to the composed member. |
| Fragile base class | Vulnerable — changes to the base may break derived classes. | Resilient — internal changes to the component remain hidden. |
Inheritance vs. Composition across key design dimensions.
DESIGN HEURISTIC
SECTION 8
Connection to Advanced OOP Concepts
The introductory notion of public inheritance as an is-a relationship is the foundation upon which several more advanced C++ features are built. Understanding where this introduction ends and advanced territory begins helps you plan your learning trajectory.
| Introductory Concept | Advanced Extension | Key New Idea |
|---|---|---|
| Single public inheritance | Multiple inheritance | A class derives from two or more bases; introduces the diamond problem and virtual inheritance. |
| Virtual functions with default body | Pure virtual functions & abstract classes | A function declared = 0 has no default body; the base class cannot be instantiated, enforcing that every derived class provides an implementation. |
| Calling area() through Shape* | Runtime type identification (RTTI) | dynamic_cast and typeid allow querying the actual derived type at runtime — useful but often a sign of design issues. |
| Public inheritance (is-a) | Private inheritance (implemented-in-terms-of) | Models implementation reuse without substitutability; base members become private in the derived class. |
| Inheritance hierarchies | CRTP (Curiously Recurring Template Pattern) | Static polymorphism via templates — the derived class passes itself as a template argument to the base, enabling compile-time dispatch. |
How introductory inheritance concepts connect to advanced C++ OOP topics.
Each of these advanced topics builds directly upon the mental model established here. Mastering the is-a test and understanding how public inheritance, constructors, access specifiers, and virtual dispatch interact will make these extensions feel like natural progressions rather than new paradigms. In particular, the Liskov Substitution Principle remains the guiding constraint: any advanced use of inheritance that violates substitutability is misusing the mechanism, regardless of syntactic correctness.
SECTION 9
Practice Problems
Question 1: CONCEPTUAL
class Penguin : public Bird { void fly(); }; where Bird has a virtual fly() method. Explain why this design may violate the Liskov Substitution Principle and suggest a better hierarchy.Show answer
bird->fly() expecting meaningful flight behavior, a Penguin object would need to throw an exception or silently do nothing — either violates the base-class contract. A better design separates flight from bird-ness: create a Bird base without fly(), then derive FlyingBird : public Bird (which adds fly()) and Penguin : public Bird (which does not). This way, no Bird pointer is expected to fly unless the pointer is actually to a FlyingBird.Question 2: BASIC
Square that publicly inherits from Shape, stores a side length, and overrides area() to return side². Use the override specifier.
Base class: class Shape { protected: std::string color; public: Shape(const std::string& c) : color(c) {} virtual double area() const { return 0.0; } virtual ~Shape() = default; };Show answer
class Square : public Shape {
double side;
public:
Square(double s, const std::string& c)
: Shape(c), side(s) {}
double area() const override {
return side * side;
}
};
Square publicly inherits from Shape, delegating color initialization to Shape's constructor via the member initializer list. The override keyword ensures that if the signature does not match a base virtual, the compiler will emit an error. Note that while a Square is-a Shape passes the is-a test, inheriting Square from Rectangle would be problematic due to LSP violations (independent width/height setters break the square invariant).Question 3: INTERMEDIATE
class Vehicle {
protected:
int speed;
public:
Vehicle(int s) : speed(s) {}
virtual void describe() const {
std::cout << "Vehicle at " << speed << " mph\n";
}
virtual ~Vehicle() = default;
};
class Car : public Vehicle {
int doors;
public:
Car(int s, int d) : Vehicle(s), doors(d) {}
void describe() const override {
std::cout << "Car with " << doors << " doors at " << speed << " mph\n";
}
};
What is the output of the following?
Car c(60, 4);
Vehicle* vp = &c;
vp->describe();
Vehicle v = c; // slicing!
v.describe();Show answer
vp->describe(), outputs "Car with 4 doors at 60 mph" because dynamic dispatch selects Car's override via the vtable — vp points to a Car object. The second call, v.describe(), outputs "Vehicle at 60 mph" because Vehicle v = c performs object slicing: only the Vehicle sub-object is copied into v, discarding Car-specific data and the Car vtable pointer. The static type of v is Vehicle, so Vehicle::describe() is called. This is a classic pitfall: always use pointers or references for polymorphic behavior.Question 4: APPLIED
Logger base class defines a virtual method void log(const std::string& msg). You need a FileLogger that writes to disk and a ConsoleLogger that writes to std::cout. Sketch the class hierarchy (data members, constructors, and the overridden log method) and explain why inheritance is appropriate here.Show answer
class Logger {
protected:
std::string prefix;
public:
Logger(const std::string& p) : prefix(p) {}
virtual void log(const std::string& msg) {
// default: no-op or print to stderr
}
virtual ~Logger() = default;
};
class FileLogger : public Logger {
std::ofstream out;
public:
FileLogger(const std::string& p, const std::string& path)
: Logger(p), out(path, std::ios::app) {}
void log(const std::string& msg) override {
out << prefix << ": " << msg << '\n';
}
};
class ConsoleLogger : public Logger {
public:
ConsoleLogger(const std::string& p) : Logger(p) {}
void log(const std::string& msg) override {
std::cout << prefix << ": " << msg << '\n';
}
};
Inheritance is appropriate because a FileLogger is-a Logger and a ConsoleLogger is-a Logger — both satisfy the base contract (accept a message, persist it somewhere). Client code that accepts a Logger& can use either interchangeably, satisfying LSP. The shared prefix handling avoids code duplication.Question 5: CRITICAL THINKING
Show answer
SUMMARY
Lesson Summary
Summary
Inheritance in C++ models is-a relationships by allowing a derived class to acquire the data members and functions of a base class while extending or overriding them. The syntax class Derived : public Base establishes public inheritance, meaning base public and protected members retain their access levels. A virtual function in the base enables dynamic dispatch — the runtime selects the correct override based on the actual object type rather than the pointer type, enabling polymorphic code that operates through base-class references.
The Liskov Substitution Principle provides the formal test: a derived object must be usable anywhere its base type is expected without breaking correctness. When the is-a relationship does not hold — as with Stack inheriting from Vector or Square inheriting from Rectangle — composition (has-a) is the correct alternative. Always apply the is-a litmus test, verify LSP compliance, declare destructors virtual in polymorphic bases, and use the override specifier to catch signature mismatches at compile time. These foundational practices prepare you for advanced topics including abstract classes, multiple inheritance, and static polymorphism via templates.