Implementing Class Hierarchies
Learn basic inheritance in C++ to create hierarchical relationships between classes.
What Is Basic Inheritance in C++?
Basic inheritance is the language feature that lets one class declare "I already have everything that other class has". You write a single clause, : public SomeClass, in the declaration of the new class, and from that moment every member the older class declared is also a member of the new one. The new class then adds whatever it needs on top.
That is the whole mechanism. Everything else in this lesson follows from it: how the two classes are named, when the clause is the right tool, what happens when you stack several of them, and what the clause deliberately does not do.
The Two Ends of an Inheritance Relationship
An inheritance relationship always runs between exactly two classes at a time, and each end has a settled vocabulary. Both ends collected three names over the decades, and all three are still in daily use:
| End of the relationship | Names you will see | What it contributes |
|---|---|---|
| The class being inherited from | parent class, base class, superclass | The members every class below it will have |
| The class doing the inheriting | child class, derived class, subclass | Its own extra members, added to what it received |
The relationship these two ends form is an is-a relationship, and that phrase is worth reading literally. Saying BugReport derives from Ticket is a claim that a bug report is a ticket, and the compiler will hold you to it: from then on, anything the language allows you to do with a ticket, it will allow you to do with a bug report.
The three names at each end are interchangeable, so pick one and stay consistent within a codebase. This course uses "base" and "derived", because those are the words that appear in compiler diagnostics and in the C++ standard itself.
The Class You Start From
Inheritance always begins with a class that already exists and already works. Here is a class representing a work ticket in an issue tracker. It carries only what every ticket carries, no matter what kind of ticket it turns out to be:
#include <iostream>
#include <string>
class Ticket
{
// Members are public here only to keep the examples short
public:
std::string m_reference{ "UNASSIGNED" };
int m_priority{ 3 };
void printHeader() const
{
std::cout << m_reference << " (priority " << m_priority << ")" << '\n';
}
};
int main()
{
Ticket incident{};
incident.m_reference = "TCK-4187";
incident.m_priority = 2;
incident.printHeader();
return 0;
}
TCK-4187 (priority 2)
Nothing here is about inheritance yet. Ticket is an ordinary class with two data members and one member function, and it compiles and runs on its own.
Every example in this lesson leaves its data members public. Real classes keep their data private, and the section of this chapter on access specifiers explains exactly how inheritance and access levels interact. Public members here just let each example be a class definition and a `main()` rather than a class definition, a set of setters, and a `main()`.
Choosing Between Copying, Composing, and Inheriting
Now suppose the tracker needs to record bug reports, which carry a reference and a priority like every ticket, plus the component at fault and the build the fault was seen in. The reference and priority already exist in Ticket, so there are three ways to get them into the new class, and only one of them is right:
| Approach | What you write | What it costs |
|---|---|---|
| Copy the members | Declare m_reference and m_priority again in the new class |
Two definitions of the same idea, kept in sync by hand forever |
Hold a Ticket member |
Give the new class a Ticket m_ticket{}; data member |
Callers reach the reference through report.m_ticket.m_reference, and the class is not usable where a ticket is expected |
Derive from Ticket |
Write : public Ticket in the declaration |
Nothing, when the is-a claim is actually true |
The middle row is composition, the technique from the previous chapter, and the way to rule it out is to read the relationship aloud in both directions. "Does a bug report have a ticket?" is a strange thing to say, because the bug report is not carrying a ticket around, it is one. "Is a bug report a ticket?" is plainly true, so inheritance is the honest description.
Before writing an inheritance clause, say "a Derived is a Base" out loud. If it sounds like a definition, inherit. If it only sounds true because the derived class happens to need the base class's data, you want composition instead, and the has-a phrasing will be the one that reads naturally.
Writing the Inheritance Clause
The clause goes between the new class's name and its opening brace: a colon, the keyword public, and the name of the base class.
class BugReport : public Ticket
The public here selects public inheritance, which keeps each inherited member at the access level the base class gave it and is by far the most common choice. C++ offers two other keywords in that position, and a later lesson in this chapter covers what they change.
With the clause in place, BugReport declares only the two members that are genuinely its own:
#include <iostream>
#include <string>
class Ticket
{
public:
std::string m_reference{ "UNASSIGNED" };
int m_priority{ 3 };
void printHeader() const
{
std::cout << m_reference << " (priority " << m_priority << ")" << '\n';
}
};
class BugReport : public Ticket
{
public:
std::string m_component{ "unknown" };
int m_buildNumber{ 0 };
void printDiagnosis() const
{
std::cout << m_reference << " traced to " << m_component
<< " in build " << m_buildNumber << '\n';
}
};
int main()
{
BugReport crash{};
crash.m_reference = "TCK-4187";
crash.m_priority = 1;
crash.m_component = "parser";
crash.m_buildNumber = 918;
crash.printHeader();
crash.printDiagnosis();
return 0;
}
TCK-4187 (priority 1)
TCK-4187 traced to parser in build 918
Count the assignments in main(). A BugReport object has four data members: m_component and m_buildNumber, which its own definition declares, plus m_reference and m_priority, which arrive from Ticket. Member functions come across on the same terms, which is why crash.printHeader() compiles even though no line of BugReport mentions printHeader.
The interesting line is inside printDiagnosis. It reads m_reference with no qualification, no Ticket:: prefix, and no accessor call, exactly as it reads its own m_component. That is the point of saying inherited members become members of the derived class: from inside BugReport, there is no seam between the two groups.
Deriving From a Class That Is Itself Derived
A base class does not have to be a class you wrote from scratch. Any class can appear after : public, including one that already inherits, and the rule applies again unchanged.
A security bug is a bug report, which is a ticket, so SecurityBug derives from BugReport and picks up two levels of members at once:
#include <iostream>
#include <string>
class Ticket
{
public:
std::string m_reference{ "UNASSIGNED" };
int m_priority{ 3 };
void printHeader() const
{
std::cout << m_reference << " (priority " << m_priority << ")" << '\n';
}
};
class BugReport : public Ticket
{
public:
std::string m_component{ "unknown" };
int m_buildNumber{ 0 };
void printDiagnosis() const
{
std::cout << m_reference << " traced to " << m_component
<< " in build " << m_buildNumber << '\n';
}
};
class SecurityBug : public BugReport
{
public:
int m_severityScore{ 0 };
void printAdvisory() const
{
std::cout << "advisory for " << m_reference << ": " << m_component
<< " scores " << m_severityScore << " out of 10" << '\n';
}
};
int main()
{
SecurityBug leak{};
leak.m_reference = "TCK-5023";
leak.m_priority = 1;
leak.m_component = "session store";
leak.m_buildNumber = 940;
leak.m_severityScore = 8;
leak.printHeader();
leak.printDiagnosis();
leak.printAdvisory();
return 0;
}
TCK-5023 (priority 1)
TCK-5023 traced to session store in build 940
advisory for TCK-5023: session store scores 8 out of 10
SecurityBug declares one data member and one member function, yet a SecurityBug object has five data members and three member functions available on it. printAdvisory reaches back through both levels in a single expression: m_reference came from Ticket, m_component from BugReport, and m_severityScore is its own.
Written out, the chain reads Ticket to BugReport to SecurityBug, and it gets more specific at every step. Ticket describes anything the tracker can hold, BugReport narrows that to defects, and SecurityBug narrows it further still. That progression from general to specific is the shape a healthy hierarchy takes, and it is what makes the top of a chain worth investing in: whatever you put in Ticket is available everywhere below it.
Siblings Share an Ancestor, Not an Interface
Two classes can derive from the same base without gaining any relationship to each other. Add a FeatureRequest class beside BugReport and both are tickets, but neither is the other, and nothing flows sideways between them.
The following program does not compile, and the diagnostic is worth reading:
#include <iostream>
#include <string>
class Ticket
{
public:
std::string m_reference{ "UNASSIGNED" };
int m_priority{ 3 };
};
class BugReport : public Ticket
{
public:
std::string m_component{ "unknown" };
int m_buildNumber{ 0 };
};
class FeatureRequest : public Ticket
{
public:
int m_votes{ 0 };
};
int main()
{
BugReport crash{};
crash.m_reference = "TCK-4187";
crash.m_votes = 12;
std::cout << crash.m_reference << '\n';
return 0;
}
Trimmed diagnostic:
s.cpp: In function 'int main()':
s.cpp:28:11: error: 'class BugReport' has no member named 'm_votes'
28 | crash.m_votes = 12;
| ^~~~~~~
m_reference on the line above was fine, because it travels down from Ticket. m_votes never enters BugReport at all, because FeatureRequest is not on any path between BugReport and its base. Inheritance moves members downward, never sideways, and sibling classes are free to specialise in completely different directions for exactly that reason.
One Edit, Every Derived Class
The payoff for putting shared members at the top of a hierarchy shows up the day the shared members change. Adding a single function to Ticket gives it to BugReport, FeatureRequest, and SecurityBug at once, with no edit to any of them:
#include <iostream>
#include <string>
class Ticket
{
public:
std::string m_reference{ "UNASSIGNED" };
int m_priority{ 3 };
bool isUrgent() const { return m_priority <= 2; }
};
class BugReport : public Ticket
{
public:
std::string m_component{ "unknown" };
int m_buildNumber{ 0 };
};
class FeatureRequest : public Ticket
{
public:
int m_votes{ 0 };
};
class SecurityBug : public BugReport
{
public:
int m_severityScore{ 0 };
};
int main()
{
BugReport crash{};
crash.m_reference = "TCK-4187";
crash.m_priority = 1;
FeatureRequest darkTheme{};
darkTheme.m_reference = "TCK-4402";
darkTheme.m_priority = 4;
SecurityBug leak{};
leak.m_reference = "TCK-5023";
leak.m_priority = 1;
std::cout << crash.m_reference << " -> " << (crash.isUrgent() ? "escalate" : "queue") << '\n';
std::cout << darkTheme.m_reference << " -> " << (darkTheme.isUrgent() ? "escalate" : "queue") << '\n';
std::cout << leak.m_reference << " -> " << (leak.isUrgent() ? "escalate" : "queue") << '\n';
return 0;
}
TCK-4187 -> escalate
TCK-4402 -> queue
TCK-5023 -> escalate
isUrgent is defined once, in one class, and three unrelated-looking types answer the question correctly. Note that SecurityBug receives it without BugReport doing anything, since a chain passes members all the way down.
Compare that with the first row of the earlier table. Had each class copied m_priority and its own urgency rule, this change would have meant three edits, three chances to write < 2 in one place and <= 2 in another, and a bug that only shows up for whichever class was forgotten. The same argument covers fixes: correct a mistake in a base class member function and every derived class is corrected in the same commit.
Looking Forward
Every object in this lesson was default-constructed and then filled in field by field, which is why no constructor appeared anywhere. That was deliberate: a derived class cannot initialise inherited members directly in its own constructor, because the base class portion of the object is built first and is already complete before the derived constructor body starts. The next two lessons take that apart, first establishing the order in which a hierarchy's constructors run, then showing the syntax a derived constructor uses to pass arguments up to its base.
After that, the chapter revisits the public members these examples relied on. Once m_reference is private in Ticket, the question of what a derived class may touch becomes a real one, and protected enters the picture as the access level that means "derived classes yes, everyone else no".
Key Terminology
- Base class: the class being inherited from, also called the parent class or superclass. It supplies members to every class derived from it.
- Derived class: the class doing the inheriting, also called the child class or subclass. It gains the base class's members and adds its own.
- Public inheritance: the form written
class Derived : public Base, which keeps each inherited member at the access level the base class assigned to it. - Is-a relationship: the claim an inheritance clause makes, that an object of the derived type is also an object of the base type. Its counterpart is the has-a relationship, which calls for composition.
- Inheritance chain: a sequence of classes in which each derives from the one before, so members declared at the top are available at every level below.
- Direct base: the class named in a derived class's inheritance clause. A class reached further up the chain, such as
TicketfromSecurityBug, is an indirect base.
Summary
| You want to | Write |
|---|---|
Give Derived everything Base has |
class Derived : public Base |
Add members specific to Derived |
Declare them in Derived's body as usual |
Use an inherited member inside Derived |
Name it directly, with no qualification |
| Extend a hierarchy by another level | Derive from the derived class, using the same clause |
- Inheritance connects two classes at a time. The class being inherited from is the base, parent, or superclass; the class inheriting is the derived, child, or subclass.
- The clause is a colon, an access keyword, and the base class name, placed after the derived class's name:
class BugReport : public Ticket. - Both member variables and member functions cross over, and they become members of the derived class. A derived object therefore has its own members plus everything the base declared.
- Inheritance describes an is-a relationship. Test a candidate by reading it aloud in both directions; if "has a" is the phrasing that fits, use composition instead.
- Chains are just the same rule applied repeatedly. A class derived from a derived class holds members from every level above it, and hierarchies get more specific as they get deeper.
- Two classes derived from the same base are unrelated to each other. Members travel down a chain, never sideways between siblings.
- Because shared members are defined once, changing or fixing a base class member updates every derived class at the same time, which is the main practical reason to reach for inheritance over copying.
How did you find this lesson?
Your rating helps us improve the content.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Implementing Class Hierarchies - Quiz
Test your understanding of the lesson.
Practice Exercises
Basic Inheritance
Create a class hierarchy using inheritance. Practice defining base classes and derived classes that inherit and extend functionality.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!