Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 47

Public Inheritance: Modeling "Is-A" Relationships

Recommended resources

Introduction

Hello! In our last lesson, we mastered const-correctness, a crucial practice for writing safe and self-documenting code. You now have a solid foundation in designing individual classes with well-defined interfaces and behaviors.

This lesson marks our entry into the next major pillar of Object-Oriented Programming (OOP): Inheritance. Inheritance is the mechanism that allows us to create new classes that are based on existing ones, inheriting their attributes and behaviors. This enables you to build hierarchies of related classes, promote code reuse, and model real-world relationships in your software.

Our focus today will be on implementing the most common type of relationship: the "is-a" relationship, using public inheritance. By the end of this lesson, you'll be able to define a new class that extends a base class, understand how member access works across this hierarchy, and appreciate when to use inheritance in your designs.


1. What is Inheritance? The "Is-A" Relationship

At its core, inheritance allows you to define a general base class (also called a parent or superclass) that contains common functionality, and then create more specific derived classes (also called child or subclasses) that inherit this functionality and add their own unique features.

The primary benefit, as with many programming constructs, is avoiding code duplication. If multiple classes share common features, you can place those features in a base class and have the other classes inherit from it.

The relationship formed by public inheritance is called an "is-a" relationship. For example, a Dog is an Animal. A Car is a Vehicle. A Student is a Person. The derived class is a specialized version of the base class.

This image provides a simple, clear visualization of this concept.

C++ Inheritance Diagram: Animal and Dog Classes
A `Dog` is a derived class that inherits from the `Animal` base class. It gets the general `eat()` and `sleep()` behaviors from `Animal` and adds its own specific `bark()` behavior.

To start, let's watch a short video that explains this core idea.

Inheritance in C++

This clip from The Cherno introduces the concept of inheritance and its primary motivation: avoiding code duplication by creating class hierarchies.

Watch the first 1 minute and 18 seconds of the video. Focus on the explanation of a base class containing common functionality and subclasses branching off from it.

"Is-A" vs. "Has-A"

It's crucial to distinguish the "is-a" relationship from another common one: the "has-a" relationship (also known as composition).

  • Is-A (Inheritance): A Student is a Person. You use public inheritance.
  • Has-A (Composition): A Car has a Engine. The Car class would contain an Engine object as a member variable, but a Car is not a type of Engine.

Making this distinction is a fundamental part of object-oriented design. The following reading explains this difference clearly.

1.13 Inheritance in C++

This article from Runestone Academy defines the 'IS-A Relationship' and contrasts it with the 'HAS-A Relationship', which is a critical design concept.

First, read the introductory paragraphs of the article, down to where it says 'Logic Gates and Circuits'. Then, skip down to the subsection 'Building Circuits' and read the three paragraphs that explicitly define and contrast the 'IS-A' and 'HAS-A' relationships.


2. Implementing Public Inheritance in C++

Now that we understand the concept, let's see how to implement it. The syntax is straightforward: after the derived class name, you use a colon, the public keyword, and the name of the base class.

class Base {
    // ... base class members ...
};

class Derived : public Base {
    // ... derived class members ...
};

Let's return to the video from The Cherno, which provides a perfect demonstration. It starts by showing two classes with duplicated code and then refactors them to use inheritance, making the code much cleaner.

Inheritance in C++

This segment demonstrates the syntax for public inheritance and shows how it eliminates redundant code.

Watch from 01:18 to 04:03. Observe how the Player class is changed from a standalone class with copied code to a class that inherits from Entity using the : public Entity syntax. Notice his point that the Player class now is an Entity and contains all its members.

When Player inherits from Entity, it gets all of Entity's members. The Player object will contain the x and y member variables from Entity in addition to its own name member.


3. Inheritance and Access Specifiers

How a derived class can access members of its base class is governed by access specifiers: public, private, and a new one, protected.

  • public: Accessible by anyone.
  • private: Accessible only by members (and friends) of the same class. Derived classes cannot access private members of the base class.
  • protected: Accessible by members of the same class, its friends, and its derived classes. It is inaccessible from outside the class hierarchy.

The article "Inheritance and access specifiers" from Learn C++ is an excellent resource for this topic.

24.5 — Inheritance and access specifiers

This reading will explain the protected access specifier and detail how member access works under public inheritance.

Read the following sections from the article: The introduction, which recaps public and private. 'The protected access specifier', which explains this new keyword. 'Public inheritance', which is the most important part and directly addresses our learning goal. Pay close attention to the table showing what happens to inherited members.

Here is a summary of how public inheritance affects the access level of inherited members in the derived class:

Access specifier in Base Class Resulting Access in Derived Class
public public
protected protected
private Inaccessible

This means that with public inheritance, the public interface of the base class remains public in the derived class, which is exactly what we want for modeling an "is-a" relationship.


4. Under the Hood: The Memory Layout of Inheritance

Given your interest in how things work at a lower level, let's explore what inheritance means for an object's memory layout. When you create an object of a derived class, the compiler typically lays it out in memory by placing the data members of the base class first, followed by the data members of the derived class.

Conceptual memory layout of a Student object, which inherits from Person. The Person part comes first.

This memory layout is precisely what makes the "is-a" relationship work. Since a Student object literally contains a Person object at its beginning, a pointer to a Student (Student*) can be safely and implicitly converted to a pointer to a Person (Person*)—the address is the same.

The following video demonstrates this principle by simulating C++ inheritance using only C structs and function pointers. This provides a clear mechanical model of what the C++ compiler does for you automatically.

C++ Inheritance Explained: V-Tables, Virtual Destructors & Edge Cases

This clip from DeepDiveDev provides a fantastic 'under the hood' look at inheritance by simulating it in C. It reveals the memory layout that makes the 'is-a' relationship possible.

Watch from 07:15 to 08:11. The key insight is how embedding the 'base' struct as the first member of the 'derived' struct guarantees that their memory addresses are identical, allowing for safe pointer casting—the C equivalent of an 'is-a' relationship.


5. Hands-On Exercise: Creating a Class Hierarchy

Let's apply these concepts. You will create a simple hierarchy of Shape classes.

Instructions:

  1. Define a Shape base class in a file named Shape.hpp.
    • It should have a protected member: std::string m_name.
    • It should have a public constructor that takes a const std::string& name to initialize m_name.
    • It should have a public const member function std::string getName() const that returns the name.
    • It should have a public const member function void print() const that prints "I am a [name]".
  2. Define a Circle derived class in a file named Circle.hpp.
    • It should publicly inherit from Shape.
    • It should have a private member: double m_radius.
    • Its public constructor should take a double radius and initialize the base class part with the name "Circle" and also initialize its own m_radius member. The syntax for calling the base class constructor is Circle(double radius) : Shape("Circle"), m_radius(radius) {}.
    • It should have a public const member function double getArea() const that calculates and returns the area (). You can use 3.14159 for .
  3. In main.cpp, include Circle.hpp, create a Circle object, and test its functionality.
    • Call the print() method (inherited from Shape).
    • Call the getName() method (inherited from Shape).
    • Call the getArea() method (specific to Circle).
    • Print the results to the console.

Starter Code (main.cpp):




#include <iostream>
#include "Circle.hpp" // Make sure to create this file and Shape.hpp

int main() {
    // 1. Create a Circle object with a radius of 5.0
    Circle myCircle(5.0);

    // 2. Call the inherited print() method
    std::cout << "Calling print(): ";
    myCircle.print();
    std::cout << std::endl;

    // 3. Call the inherited getName() method
    std::cout << "Name of the shape: " << myCircle.getName() << std::endl;

    // 4. Call the Circle-specific getArea() method
    std::cout << "Area of the circle: " << myCircle.getArea() << std::endl;

    return 0;
}
Solution

Shape.hpp




#pragma once // Header guard
#include <string>
#include <iostream>

class Shape {
protected:
    std::string m_name;

public:
    // Constructor to initialize the name
    Shape(const std::string& name) : m_name(name) {}

    // Public getter for the name
    std::string getName() const {
        return m_name;
    }

    // Public print method
    void print() const {
        std::cout << "I am a " << m_name;
    }
};

Circle.hpp




#pragma once // Header guard
#include "Shape.hpp"

class Circle : public Shape {
private:
    double m_radius;

public:
    // Constructor calls the base class constructor using an initializer list
    Circle(double radius) : Shape("Circle"), m_radius(radius) {}

    // Circle-specific method to calculate area
    double getArea() const {
        return 3.14159 * m_radius * m_radius;
    }
};

Expected Output of main.cpp:

Calling print(): I am a Circle
Name of the shape: Circle
Area of the circle: 78.5397

Conclusion

Congratulations! You've just taken your first and most important step into inheritance with C++. You can now build class hierarchies that effectively model "is-a" relationships, promoting cleaner, more maintainable, and reusable code.

Key Takeaways:

  • Inheritance models an "is-a" relationship where a derived class is a specialized version of a base class.
  • Public inheritance (class Derived : public Base) is used to create this relationship.
  • A derived class inherits all members from its base, but can only directly access the public and protected ones. private base members are inaccessible.
  • The protected access specifier allows access within a class and by its derived classes, but not by the general public.
  • An object of a derived class physically contains its base class's members, which enables a derived class pointer to be treated as a base class pointer.

Next Up

Now that you can build class hierarchies, you might wonder: what's the most powerful thing I can do with them? The answer is polymorphism, the second major concept in this module. In the next lesson, you'll learn how to use virtual functions to treat objects of different derived classes in a uniform way through a single base class interface. This will unlock a new level of flexibility and power in your C++ programs.