C++ • Object-Oriented Programming

Inheritance — Use inheritance for is-a relationships (intro)

Model real-world hierarchies in C++ by letting derived classes extend and specialize base-class behavior.

Programming braces with a command terminal and binary digits.
Inside this lesson 10 sections 5 questions 5 figures

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

Ole-Johan Dahl and Kristen Nygaard designed Simula 67 at the Norwegian Computing Center. It was the first language to support classes, subclasses, and virtual methods — the conceptual ancestors of C++ inheritance.

1979 • Bjarne Stroustrup Begins 'C with Classes'

Inspired directly by Simula's class hierarchy, Stroustrup added class-based inheritance to C, creating the precursor to C++. His goal was Simula-like abstraction with C-level performance.

1985 • The C++ Programming Language, 1st Edition

Stroustrup's book formalized public, protected, and private inheritance, along with virtual functions. The is-a guideline became a core design principle taught alongside the language.

1994 • The Liskov Substitution Principle (LSP) Formalized

Barbara Liskov and Jeannette Wing published a rigorous behavioral subtyping criterion. LSP gave inheritance its formal contract: a derived object must be substitutable wherever a base object is expected.

2011–2020 • Modern C++ Refinements

C++11 introduced the override and final specifiers, strengthening compile-time checks on inheritance hierarchies. Subsequent standards continued to refine virtual dispatch, move semantics with base classes, and concepts for constraining template-based alternatives.

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.

1

Is-A Relationship

Public inheritance models the is-a relationship: every instance of the derived class should be semantically substitutable for an instance of the base class. If the sentence "A Derived is-a Base" sounds wrong, inheritance is likely the wrong tool.
2

Code Reuse via Generalization

Common data members and functions are defined once in the base class and automatically available in every derived class. This eliminates duplication and centralizes maintenance — fix a bug in the base, and all derived classes benefit.
3

Access Control: public, protected, private

The access specifier on the inheritance declaration controls how base-class members are exposed through the derived class. Public inheritance preserves the original access levels; protected and private inheritance restrict them.
4

The Liskov Substitution Principle

A derived object must be usable anywhere its base type is expected without altering the correctness of the program. Violating LSP leads to fragile hierarchies and subtle runtime bugs.
5

Is-A vs. Has-A

Inheritance expresses is-a; composition expresses has-a. A Car has-a Engine (composition), but a SportsCar is-a Car (inheritance). Choosing the wrong relationship creates tightly coupled, hard-to-maintain designs.
✦

KEY TAKEAWAY

Think of a base class as a blueprint for a family of products. A Shape blueprint specifies that every shape has a color and can compute its area. When you derive Circle from Shape, you inherit the color field for free and add a radius. The factory line (compiler) stamps out circles that include everything a shape already provides — plus the circle-specific additions. If you tried to inherit from Engine instead, the blueprint analogy would collapse: a Circle is not an Engine. That intuitive absurdity is exactly the test the is-a guideline provides.

SECTION 3

Visual Explanation — Inheritance Hierarchy

Inheritance Hierarchy: Shape → Circle, RectangleShape (Base Class)# color : string+ getColor() : string+ area() : double# = protected, + = publicpublic inheritancepublic inheritanceCircle (Derived)- radius : double+ Circle(r, c)+ area() override : double → π × radius²Rectangle (Derived)- width : double- height : double+ area() override : double → width × heightIS-A CHECK"A Circle is-a Shape" ✓ "A Rectangle is-a Shape" ✓Both derived classes pass the substitution test for Shape.
The diagram shows a Shape base class with a protected 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 ModeBase public members becomeBase protected members becomeBase private members
publicpublic in Derivedprotected in Derivedinaccessible
protectedprotected in Derivedprotected in Derivedinaccessible
privateprivate in Derivedprivate in Derivedinaccessible

How the inheritance access specifier transforms base-class member visibility in the derived class.

ℹ

Why private members are inaccessible

Private base-class members still exist in the derived object's memory, but they can only be accessed through the base class's own member functions. This enforces encapsulation: the base class retains exclusive control over its invariants, even from its own children.

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.

Is-A Decision Flowchart: Should You Use Inheritance?Does "Derived IS-A Base" make sense?YESCan Derived substitute for Basein ALL contexts? (LSP)YESDo you need polymorphic behavior?YES✓ USE PUBLIC INHERITANCENOUse CompositionNORedesign thehierarchyMAYBEConsidercomposition firstCommon Is-A Mistakes✗ Stack : VectorA Stack is NOT a Vector — it restricts operations (has-a).✗ Square : RectangleWidth/height independence breaks LSP for Square.✓ Dog : AnimalA Dog can substitute for Animal in all contexts.✓ SavingsAccount : AccountExtends behavior without breaking the base contract.
The flowchart traces three questions — the is-a test, the LSP test, and a polymorphism need check. The lower box lists classic is-a mistakes (Stack inheriting from Vector, Square inheriting from Rectangle) alongside correct examples.

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.
  1. Step 1: Identify the general concept (Shape, Animal, Account) and define it as the base class with shared data and behavior.
  2. Step 2: Apply the is-a litmus test: does every instance of the derived class genuinely qualify as an instance of the base?
  3. Step 3: Declare the derived class using public inheritance, adding specialized members and overriding virtual functions as needed.
  4. 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. 1

    Step 1 — Define the Base Class (Shape)

    We define 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; };
    Base class Shape defined with virtual area() and virtual destructor.
  2. 2

    Step 2 — Derive Circle (is-a Shape)

    Circle publicly inherits from Shape, adds a 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; } };
    Circle inherits color and getColor() from Shape and overrides area() to return π × r².
  3. 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 overrides area() to return width × height.
  4. 4

    Step 4 — Use Polymorphically Through Base Pointers

    We store both derived objects in a 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'; } }
    Output: red shape, area = 78.5398 | blue shape, area = 24
  5. 5

    Step 5 — Verify the Is-A Guarantee

    The loop body never mentions Circle or Rectangle — it works entirely through 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).
    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 AxisInheritance (is-a)Composition (has-a)
CouplingTight — derived class depends on base-class implementation details.Loose — composed object interacts only through the component's public interface.
PolymorphismBuilt-in via virtual functions; derived objects substitutable for base.Not automatic; requires explicit forwarding or the Strategy pattern.
Flexibility at runtimeFixed at compile time — the base class cannot be changed after object creation.High — composed members can be swapped at runtime via pointers/references.
Code reuseAutomatic — all non-private members visible in derived class.Explicit — must forward or delegate calls to the composed member.
Fragile base classVulnerable — 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

Use inheritance when the relationship is genuinely is-a and you need polymorphic substitution. Use composition for everything else. A useful real-world analogy: an engineer inherits traits from their species (Human is-a Mammal) — that is a permanent taxonomic fact. But they have a laptop (Engineer has-a Laptop) — a relationship that can be changed. Modeling "has-a" with inheritance would imply that the engineer is a laptop, which is clearly nonsensical.

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 ConceptAdvanced ExtensionKey New Idea
Single public inheritanceMultiple inheritanceA class derives from two or more bases; introduces the diamond problem and virtual inheritance.
Virtual functions with default bodyPure virtual functions & abstract classesA 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 hierarchiesCRTP (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

A colleague proposes the following hierarchy: 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
The LSP requires that any subtype be substitutable for its parent type without altering program correctness. If client code calls 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

Given the following base class, write a derived class 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

Consider the following code: 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
The first call, 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

You are designing a logging library. A 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

A student argues: "I should always make every member function virtual in my base class so derived classes have maximum flexibility." Present a well-reasoned counterargument, citing at least two concrete drawbacks of making all functions virtual.
Show answer
While maximizing flexibility sounds appealing, making every function virtual has significant drawbacks. First, performance: virtual calls require an indirect lookup through the vtable, preventing the compiler from inlining the function body — a meaningful cost in tight loops or performance-critical systems code. Second, contract weakening: marking a function virtual is a declaration that derived classes may change its behavior. If the base class relies on a non-virtual function maintaining a specific invariant (e.g., a template method that calls virtual helpers in a controlled sequence), making it virtual allows derived classes to break that invariant, violating encapsulation. Third, cognitive load: a virtual function signals to maintainers "this is a customization point." If everything is virtual, no function is special, and understanding the class's intended extension points becomes harder. The Non-Virtual Interface (NVI) idiom explicitly advocates making public functions non-virtual and providing private virtual hooks for customization. The principle: virtual should be used deliberately to mark intended customization points, not as a blanket policy.

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.

Varsity Tutors • C++ • Inheritance — Use inheritance for is-a relationships (intro)