Basic Inheritance in C++

Now that we've discussed inheritance conceptually, let's examine how it works in C++.

Inheritance in C++ occurs between classes. In an inheritance (is-a) relationship, the class being inherited from is called the parent class, base class, or superclass, and the class performing the inheritance is called the child class, derived class, or subclass.

In an inheritance hierarchy, the parent class sits at the top, with child classes inheriting from it. A class can be both a child (to a class above it) and a parent (to classes below it).

A child class inherits both capabilities (member functions) and attributes (member variables) from the parent (subject to certain access restrictions we'll explore later). These inherited variables and functions become members of the derived class.

Since child classes are complete classes in their own right, they can naturally have their own members that are unique to that class.

An Entity class

Here's a straightforward class representing a generic game entity:

#include <string>
#include <string_view>

class Entity
{
// For simplicity, we're using public members in this example
public:
    std::string m_name{};
    int m_health{};

    Entity(std::string_view name = "", int health = 100)
        : m_name{ name }, m_health{ health }
    {
    }

    const std::string& getName() const { return m_name; }
    int getHealth() const { return m_health; }
};

This Entity class represents a generic game entity, so we've only included members common to all entity types. Every entity (regardless of type, abilities, etc.) has a name and health, so these are represented here.

Note that in this example, all variables and functions are public. This is purely to keep these examples straightforward. Normally, variables would be private. We'll discuss access controls and their interaction with inheritance later in this chapter.

An Enemy class

Let's say we wanted to write a program tracking enemies in a game. Enemies need to store information specific to enemies -- for instance, we might want to track the damage they deal and their aggression level.

Here's our basic Enemy class:

class Enemy
{
// For simplicity, we're using public members
public:
    int m_damage{};
    int m_aggression{};

    Enemy(int damage = 10, int aggression = 5)
       : m_damage{ damage }, m_aggression{ aggression }
    {
    }
};

Now, we also want to track the entity's name and health, and we already have that information in our Entity class.

We have three options for adding name and health to Enemy:

  1. Add name and health directly to Enemy as members. This is probably the worst option, as we're duplicating code that already exists in Entity. Any modifications to Entity would also need to be made in Enemy.
  2. Add Entity as a member of Enemy using composition. But we should ask ourselves, "does an Enemy have an Entity"? No, it doesn't. So composition isn't the right approach.
  3. Have Enemy inherit those attributes from Entity. Remember that inheritance represents an is-a relationship. Is an Enemy an Entity? Yes, it is. So inheritance is appropriate here.

Making Enemy a derived class

To have Enemy inherit from our Entity class, the syntax is straightforward. After the class Enemy declaration, we use a colon, the word "public", and the name of the class we want to inherit from. This is called public inheritance. We'll discuss what public inheritance means in a later lesson.

// Enemy publicly inheriting Entity
class Enemy : public Entity
{
public:
    int m_damage{};
    int m_aggression{};

    Enemy(int damage = 10, int aggression = 5)
       : m_damage{ damage }, m_aggression{ aggression }
    {
    }
};

When Enemy inherits from Entity, Enemy acquires the member functions and variables from Entity. Additionally, Enemy defines two members of its own: m_damage and m_aggression. This makes sense, since these properties are specific to an Enemy, not to every Entity.

Thus, Enemy objects will have 4 member variables: m_damage and m_aggression from Enemy, and m_name and m_health from Entity.

This is simple to demonstrate:

#include <iostream>
#include <string>
#include <string_view>

class Entity
{
public:
    std::string m_name{};
    int m_health{};

    Entity(std::string_view name = "", int health = 100)
        : m_name{ name }, m_health{ health }
    {
    }

    const std::string& getName() const { return m_name; }
    int getHealth() const { return m_health; }
};

// Enemy publicly inheriting Entity
class Enemy : public Entity
{
public:
    int m_damage{};
    int m_aggression{};

    Enemy(int damage = 10, int aggression = 5)
       : m_damage{ damage }, m_aggression{ aggression }
    {
    }
};

int main()
{
    // Create a new Enemy object
    Enemy goblin{};
    // Assign a name (we can do this directly because m_name is public)
    goblin.m_name = "Goblin";
    // Print out the name
    std::cout << goblin.getName() << '\n'; // use getName() function acquired from Entity base class

    return 0;
}

This prints:

Goblin

This compiles and runs because goblin is an Enemy, and all Enemy objects have an m_name member variable and a getName() member function inherited from the Entity class.

A Boss derived class

Now let's write another class that also inherits from Entity. This time, we'll create a Boss class. A boss "is an" entity, so inheritance is appropriate:

// Boss publicly inherits from Entity
class Boss: public Entity
{
public:
    int m_phase{};
    bool m_isEnraged{};

    Boss(int phase = 1, bool enraged = false)
        : m_phase{ phase }, m_isEnraged{ enraged }
    {
    }

    void displayStatus() const
    {
        std::cout << m_name << " - Phase " << m_phase << '\n';
    }
};

Boss inherits m_name and m_health from Entity (along with the two access functions), and adds two more member variables and a member function of its own. Note that displayStatus() uses variables from both the class it belongs to (Boss::m_phase) and the parent class (Entity::m_name).

Note that Boss and Enemy have no direct relationship, even though they both inherit from Entity.

Here's a complete example using Boss:

#include <iostream>
#include <string>
#include <string_view>

class Entity
{
public:
    std::string m_name{};
    int m_health{};

    Entity(std::string_view name = "", int health = 100)
        : m_name{ name }, m_health{ health }
    {
    }

    const std::string& getName() const { return m_name; }
    int getHealth() const { return m_health; }
};

// Boss publicly inherits from Entity
class Boss: public Entity
{
public:
    int m_phase{};
    bool m_isEnraged{};

    Boss(int phase = 1, bool enraged = false)
        : m_phase{ phase }, m_isEnraged{ enraged }
    {
    }

    void displayStatus() const
    {
        std::cout << m_name << " - Phase " << m_phase << '\n';
    }
};

int main()
{
    Boss dragon{ 2, true };
    dragon.m_name = "Fire Dragon"; // we can do this because m_name is public

    dragon.displayStatus();

    return 0;
}

This prints:

Fire Dragon - Phase 2

Inheritance chains

It's possible to inherit from a class that is itself derived from another class. Nothing special or noteworthy happens when doing this -- everything proceeds as in the examples above.

For example, let's write a RaidBoss class. A RaidBoss is a Boss, which is an Entity. We've already written a Boss class, so let's use that as the base class from which to derive RaidBoss:

class RaidBoss: public Boss
{
public:
    // This RaidBoss requires a group to defeat
    int m_minPlayers{};
};

All RaidBoss objects inherit the functions and variables from both Boss and Entity, and add their own m_minPlayers member variable.

By building such inheritance chains, we can create a hierarchy of reusable classes that are very general (at the top) and become increasingly specific at each level of inheritance.

Why is this kind of inheritance useful?

Inheriting from a base class means we don't need to redefine the information from the base class in our derived classes. We automatically receive the member functions and member variables of the base class through inheritance, and simply add the additional functions or member variables we want. This not only saves work but also means that if we ever update or modify the base class (e.g., add new functions or fix a bug), all of our derived classes automatically inherit the changes!

For example, if we ever added a new function to Entity, then Boss, RaidBoss, and Enemy would automatically gain access to it. If we added a new variable to Boss, then RaidBoss would also gain access to it. This allows us to build new classes in an efficient, intuitive, and maintainable way!

Conclusion

Inheritance allows us to reuse classes by having other classes inherit their members. In future lessons, we'll continue exploring how this works.

Summary

Inheritance in C++ occurs between classes through an "is-a" relationship, where a derived class (child, subclass) inherits capabilities (member functions) and attributes (member variables) from a base class (parent, superclass). The derived class automatically receives base class members and can add its own unique members.

Public inheritance is specified using the syntax class Derived : public Base, establishing that Derived publicly inherits from Base. The derived class inherits both member functions and member variables from the base class, which become members of the derived class subject to access restrictions.

Inheritance chains allow classes to inherit from classes that themselves derive from other classes, creating hierarchies that progress from general base classes to increasingly specific derived classes. For example, RaidBoss can inherit from Boss, which inherits from Entity, allowing RaidBoss to access members from both Boss and Entity.

Reusability benefits include avoiding code duplication by defining common functionality once in the base class, automatic propagation of base class updates to all derived classes, and the ability to add new functions or fix bugs in the base class with all derived classes immediately benefiting from the changes.

Multiple derived classes can inherit from the same base class (like Enemy and Boss both inheriting from Entity) without having any direct relationship to each other. This allows sharing common functionality while maintaining separate, specialized implementations for each derived class type.

Inheritance provides an efficient, intuitive, and maintainable way to build new classes by extending existing ones, enabling code reuse and establishing clear hierarchical relationships between related types.