At the end of Chapter 16 we sketched an Enemy class. A real game doesn't usually have one kind of enemy, though — it has several. A chaser that runs straight at the player. A shooter that keeps its distance and fires projectiles. A bomber that dives in and explodes. All three are enemies. They all have health, a position, and a need to be updated each frame. But the way each one moves and attacks is different.
You could write three completely separate classes and have each repeat all the health-and-position boilerplate. That works, but it's miserable. Inheritance is C++'s answer. You write the shared parts once, in a base class, and each specific enemy type inherits that base and adds its own twist on top.
In this chapter, we will:
- Meet inheritance and the is-a relationship
- Write a base class and derived classes
- Use
protectedmembers to share data with derived classes - Chain constructors using base-class initializer lists
- See the order in which constructors and destructors run
- Meet name hiding and the
usingdeclaration fix - Hit the limits of inheritance alone — the slicing problem
- Think about when to choose inheritance vs composition
- Try an AI exercise on designing a class hierarchy
The Is-A Relationship
Inheritance models the idea that one kind of thing is a more specific version of another. A chaser is an enemy. A shooter is an enemy. This is the is-a relationship, and it's the test you should apply before reaching for inheritance. If you can say "a ChaserEnemy is an Enemy" honestly, inheritance fits. If the best you can do is "a ChaserEnemy has an Enemy" or "uses an Enemy," that's a different relationship.
In C++, the more general class is the base class (or parent class). The more specific class is the derived class (or child class).
Writing a Base Class
Here's Enemy.h:
#pragma once
class Enemy {
protected:
float x;
float y;
int health;
float speed;
public:
Enemy(float startX, float startY, int startingHealth, float moveSpeed);
void takeDamage(int amount);
bool isAlive() const;
void update(float deltaTime);
float getX() const;
float getY() const;
};
The data says protected, not private. protected means private to the outside world, but accessible to derived classes. A ChaserEnemy that inherits from Enemy can read and write x, y, health, and speed directly. Outside code still can't touch them.
#include "Enemy.h"
Enemy::Enemy(float startX, float startY, int startingHealth, float moveSpeed)
: x(startX), y(startY), health(startingHealth), speed(moveSpeed) {
}
void Enemy::takeDamage(int amount) {
health -= amount;
if (health < 0) health = 0;
}
bool Enemy::isAlive() const {
return health > 0;
}
void Enemy::update(float deltaTime) {
// Default behaviour: drift slowly downward.
y += speed * deltaTime;
}
float Enemy::getX() const { return x; }
float Enemy::getY() const { return y; }
Writing a Derived Class
Here's ChaserEnemy.h:
#pragma once
#include "Enemy.h"
class ChaserEnemy : public Enemy {
public:
ChaserEnemy(float startX, float startY);
void update(float deltaTime, float targetX, float targetY);
};
The important line is class ChaserEnemy : public Enemy. The colon introduces the inheritance clause. public Enemy says "this class inherits publicly from Enemy." ChaserEnemy doesn't redeclare any of the members from Enemy — it inherits them.
#include "ChaserEnemy.h"
#include <cmath>
ChaserEnemy::ChaserEnemy(float startX, float startY)
: Enemy(startX, startY, 50, 80.0f) {
}
void ChaserEnemy::update(float deltaTime, float targetX, float targetY) {
float dx = targetX - x;
float dy = targetY - y;
float distance = std::sqrt(dx * dx + dy * dy);
if (distance > 0.0f) {
x += (dx / distance) * speed * deltaTime;
y += (dy / distance) * speed * deltaTime;
}
}
ChaserEnemy's constructor uses its initializer list to call the Enemy constructor: Enemy(startX, startY, 50, 80.0f). This is how a derived class passes information up to its base class. The base constructor runs first, initializes x, y, health, and speed, and only then does ChaserEnemy's constructor body run.
If you don't explicitly call a base constructor, C++ will call the base's default constructor. Our Enemy has no default constructor, so ChaserEnemy must chain through explicitly. Inside ChaserEnemy::update we access x, y, and speed directly — allowed because they're protected.
Here's a second derived class, ShooterEnemy.h:
#pragma once
#include "Enemy.h"
class ShooterEnemy : public Enemy {
private:
float shootCooldown;
float timeSinceLastShot;
public:
ShooterEnemy(float startX, float startY);
void update(float deltaTime);
bool wantsToShoot() const;
void resetCooldown();
};
ShooterEnemy::ShooterEnemy(float startX, float startY)
: Enemy(startX, startY, 30, 20.0f),
shootCooldown(1.5f),
timeSinceLastShot(0.0f) {
}
void ShooterEnemy::update(float deltaTime) {
y += speed * deltaTime; // drift like a regular enemy
timeSinceLastShot += deltaTime;
}
Constructor and Destructor Order
When you create a ChaserEnemy:
- The
Enemyconstructor runs first, initializingx,y,health,speed. - Then
ChaserEnemy's own member initializer list runs. - Finally, the body of
ChaserEnemy's constructor runs.
When destroyed, the order is exactly reversed. The derived destructor runs first, then the base. This matters because a derived class depends on its base — if the base were destroyed first, the derived part would briefly exist without its foundation.
Inheritance Access Specifiers
publicinheritance: public members of the base stay public in the derived class. This preserves the is-a relationship. Use this almost every time.protectedinheritance: public and protected base members become protected in the derived class.privateinheritance: public and protected base members become private in the derived class.
Name Hiding
Here's a subtle gotcha. Enemy has void update(float deltaTime). ChaserEnemy has void update(float deltaTime, float targetX, float targetY) — three arguments. You might expect both updates to be available. They're not. As soon as you declare a function named update in ChaserEnemy, the base's update is hidden. This is called name hiding.
The fix is the using declaration:
class ChaserEnemy : public Enemy {
public:
using Enemy::update; // Bring the base's update back into view.
ChaserEnemy(float startX, float startY);
void update(float deltaTime, float targetX, float targetY);
};
The Limits of Inheritance Alone — Slicing
Say you want a mixed collection of enemies and loop over them calling update on each:
std::vector<Enemy> enemies;
enemies.push_back(ChaserEnemy(100.0f, 50.0f));
enemies.push_back(ShooterEnemy(200.0f, 50.0f));
for (auto& e : enemies) {
e.update(0.016f);
}
This code compiles and runs — but calls Enemy::update for both entries, not the derived versions. The reason is called slicing. A std::vector<Enemy> holds objects of type Enemy exactly. When you push_back a ChaserEnemy, C++ copies the Enemy part of it into the vector slot — and throws the rest away. The chaser-specific pieces are sliced off.
The same slicing happens any time you pass a derived object by value to a function expecting the base:
void damageEnemy(Enemy e) { // Pass by value — slices!
// ...
}
ChaserEnemy c(100.0f, 50.0f);
damageEnemy(c); // c is sliced into a plain Enemy as it's copied.
The trick to avoiding slicing is to work with pointers or references to the base class:
std::vector<Enemy*> enemies; // Pointers to Enemy.
enemies.push_back(new ChaserEnemy(100.0f, 50.0f));
enemies.push_back(new ShooterEnemy(200.0f, 50.0f));
for (auto* e : enemies) {
e->update(0.016f);
}
No copying, no slicing. But even here, e->update(0.016f) still calls Enemy::update — C++ looks at the pointer's declared type (Enemy*) and picks the function based on that. To make C++ pick the right function based on the actual object type, you need to mark the function as virtual. That's Chapter 20.
A Word on Multiple Inheritance
C++ allows a class to inherit from more than one base class. This is called multiple inheritance, and it brings with it the diamond problem — ambiguity when two bases share a common base. Multiple inheritance is rarely the right tool in game code. Recognize the term, know the diamond problem exists, and move on.
Inheritance vs Composition
Inheritance models is-a. A ChaserEnemy is an Enemy. A Wizard is a Character.
Composition models has-a. A Car has an Engine. A Player has an Inventory. With composition, one class owns another as a member variable. No inheritance, no base class, no hierarchy.
The rule of thumb: if you're reaching for inheritance to share code rather than to model a real is-a relationship, you probably want composition instead. The standard advice is: prefer composition, use inheritance only when you genuinely need the is-a relationship.
AI Exercise
"I'm a C++ beginner learning inheritance. I'm designing the pickups for a 2D game. There will be three kinds:
HealthPickup(restores health when collected),AmmoPickup(adds ammo when collected), andShieldPickup(grants a temporary shield when collected). All three have a position, an active/used state, and a function that runs when the player collides with them. Please sketch a class hierarchy for these pickups as header files only (no .cpp). Use public inheritance. Mark any data that derived classes need to access as protected. Explain your design choices in one paragraph, and then separately tell me whether you think inheritance is actually the right tool here, or whether composition might be a better fit."
Notice the prompt explicitly asks the AI to second-guess itself. That last question is how you avoid a response that dutifully produces a hierarchy without ever asking whether a hierarchy is the right answer.
Summary
You now have inheritance as a tool. You can write a base class, derive one or more specific classes from it, share data via protected, chain constructors through initializer lists, and understand the order in which parts of an object come and go. You've met name hiding and know how to fix it with using. You've hit the slicing problem and seen that storing derived objects in base-typed containers loses the derived part — a warning to carry forward. And you've weighed inheritance against composition.
The piece that's still missing is what the slicing section ended on. Even with pointers to the base class, calling update on a pointer still goes to the base's version, not the derived one. To get the right function called based on the object's actual type, we need polymorphism, built on the virtual keyword. That's Chapter 20.