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.
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.
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
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.
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 |
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.
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:
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.
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 with a protected string member color, a constructor, a virtual area() function, and a virtual destructor (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;
};Shape defined with virtual area() and virtual destructor.radius data member, and overrides area(). The override specifier (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;
}
};color and getColor() from Shape and overrides area() to return π × r².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;
}
};area() to return width × height.vector<Shape*> and iterate, calling area(). 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';
}
}Shape*. 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).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. |
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. |
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.
Practice Problems
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.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; };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();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.Lesson 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.