In our previous lessons, we explored how classes can inherit functions and variables from their parent, or base, classes. This powerful feature of object-oriented programming allows us to build upon existing code, promoting reusability and efficiency.
In this lesson, we will delve into how to effectively work with these inherited members. We'll uncover how to harness their potential while steering clear of some common pitfalls that can trip us up.
By the end of this lesson, you'll have a solid understanding of how to use inherited members in your programs, setting a strong foundation for more advanced topics later in the course.
We previously saw how we could define constructors for our classes. These allowed us to pass values when creating an object, to control how it gets initialized.
Things get a bit more complicated when we’re working with inheritance because we need to construct the inherited variables too.
We do this by calling the constructors that are in those classes we inherited from. If we don’t intervene in that process, we’ll use default constructors, all the way down:
#include <iostream>
using namespace std;
class Monster {
public:
Monster(){
cout << "Default Constructing Monster";
}
};
class Goblin : public Monster {
public:
Goblin(){
cout << "\nDefault Constructing Goblin";
}
};
int main(){ Goblin Bonker; }
Default Constructing Monster
Default Constructing Goblin
Within any constructor of our derived class, we can explicitly set which base constructor we want to use.
The only way we can do this is through a member initializer list. The following program replicates the previous behavior, but our Goblin
constructor is now explicitly calling the default constructor on Monster
:
#include <iostream>
using namespace std;
class Monster {
public:
Monster(){
cout << "Default Constructing Monster";
}
};
class Goblin : public Monster {
public:
Goblin() : Monster{}{
cout << "\nDefault Constructing Goblin";
}
};
int main(){ Goblin Bonker; }
Default Constructing Monster
Default Constructing Goblin
If the inherited type doesn’t have a default constructor, and we don’t specify which alternative to use, we won’t be able to create any objects using our derived class.
To control which inherited constructor we call, we need to pass arguments within the Monster{}
expression in our member initializer list.
The compiler uses these arguments to determine which constructor to use, just like it does when we construct an object in any other context.
Below, we’ve removed the default constructor from our Monster
, replacing it with a constructor that accepts an int
.
Then, within the member initializer list of our Goblin
default constructor, we pass that int
:
#include <iostream>
using namespace std;
class Monster {
public:
Monster(int Health) : mHealth{Health}{
cout << "Constructing Monster with an int";
}
int GetHealth(){ return mHealth; }
private:
int mHealth{100};
};
class Goblin : public Monster {
public:
Goblin() : Monster{150}{
cout << "\nDefault Constructing Goblin";
}
};
int main(){
Goblin Bonker;
cout << "\nHealth: " << Bonker.GetHealth();
}
Constructing Monster with an int
Default Constructing Goblin
Health: 150
Naturally, we are free to use expressions in this process, including parameters. In the following example, we’ve removed the default constructor of our Goblin
, also replacing it with one that accepts an int
.
We then forward that int
to the Monster
constructor from our member initializer list:
#include <iostream>
using namespace std;
class Monster {
public:
Monster(int Health) : mHealth{Health}{
cout << "Constructing Monster with an int";
}
int GetHealth(){ return mHealth; }
private:
int mHealth{100};
};
class Goblin : public Monster {
public:
Goblin(int Health) : Monster{Health}{
cout << "\nConstructing Goblin with an int";
}
};
int main(){
Goblin Bonker{200};
cout << "\nHealth: " << Bonker.GetHealth();
}
Constructing Monster with an int
Constructing Goblin with an int
Health: 200
Finally, let's see a slightly more complex example. Below, we’ve updated our Goblin
constructor to accept two integers.
The first is forwarded to the Monster
constructor to set the inherited mHealth
,
The second argument is used to set mDamage
, a variable that is specific to the Goblin
type:
#include <iostream>
using namespace std;
class Monster {
public:
Monster(int Health) : mHealth{Health}{
cout << "Constructing Monster with an int";
}
int GetHealth(){ return mHealth; }
private:
int mHealth{100};
};
class Goblin : public Monster {
public:
Goblin(int Health, int Damage) :
Monster{Health}, mDamage{Damage}{
cout <<
"\nConstructing Goblin with two ints";
}
int GetDamage(){ return mDamage; }
private:
int mDamage;
};
int main(){
Goblin Bonker{200, 15};
cout << "\nHealth: " << Bonker.GetHealth()
<< "\nDamage: " << Bonker.GetDamage();
}
Constructing Monster with an int
Constructing Goblin with two ints
Health: 200
Damage: 15
Consider the following code:
class Weapon {
public:
Weapon(int Damage) : mDamage{Damage} {}
private:
int mDamage;
};
class Sword : public Weapon {
public:
Sword() : Weapon{20} {}
};
When creating a Sword
object, what will be the value of its mDamage
variable?
Sometimes, we’ll want to change the value of an inherited variable, but there is no inherited constructor to let us set its initial value directly.
In these cases, we can just let the base constructor complete, and then change the value from our derived constructor. As always, to access an inherited variable, it needs to be either public
or protected
within the base class.
Below, our Monster
objects are default constructed with a Health
value of 100
. But, if we’re specifically creating a Goblin
, that type’s constructor updates the value to 150
:
#include <iostream>
using namespace std;
class Monster {
public:
int Health{100};
};
class Goblin : public Monster {
public:
Goblin(){ Health = 150; }
};
int main(){
Goblin Bonker;
cout << "Health: " << Bonker.Health;
}
Health: 150
The previous is possible because of the way constructors are called in inheritance scenarios. Specifically, base constructors are called first.
That means, within the constructors of the more derived classes, the base will have already been completed. As such, all the inherited variables are set up, and ready for us to work with.
We can see an example of this sequencing by stepping through the construction process in our debugger, or by checking the output of a program such as this:
#include <iostream>
using namespace std;
class Actor {
public:
Actor(){ cout << "Actor Constructor\n"; }
};
class Monster : public Actor {
public:
Monster(){ cout << "Monster Constructor\n"; }
};
class Goblin : public Monster {
public:
Goblin(){ cout << "Goblin Constructor\n"; }
};
int main(){
Goblin Bonker;
}
Actor Constructor
Monster Constructor
Goblin Constructor
A common way beginners try to update the value of inherited variables is simply to specify a variable with the same name and type on their derived classes:
#include <iostream>
using namespace std;
class Monster {
public:
int Health{100};
};
class Goblin : public Monster {
public:
int Health{150};
};
int main(){
Goblin Bonker;
cout << "Health: " << Bonker.Health;
}
In some cases, such as the previous example, this even seems to work:
Health: 150
But what has actually happened here is that we now have two variables - one in the scope of Monster
, and one in the scope of Goblin
.
This is very similar to the notion of shadowed variables we introduced in our earlier lesson on scope.
We can see how the approach breaks down when we make our class a little more complex. Below, we’ve moved Health
into the private section, and added a getter to our Monster
class, which Goblin
is inheriting.
#include <iostream>
using namespace std;
class Monster {
public:
int GetHealth(){ return Health; }
private:
int Health{100};
};
class Goblin : public Monster {
int Health{150};
};
int main(){
Goblin Bonker;
cout << "Health: " << Bonker.GetHealth();
}
We can now see that we’re getting the Health
value from our Monster
class, rather than our Goblin
class.
Health: 100
This is because we’re calling GetHealth()
, which is defined in the Monster
scope. And, in that scope, Health
refers to the variable with the value of 100
.
Consider the following program:
class Weapon {
public:
int GetDamage(){ return Damage; }
protected:
int Damage{10};
};
class Sword : public Weapon {
public:
Sword(){ Damage *= 2; }
};
int main(){
Sword IronSword;
int WeaponDamage{IronSword.GetDamage()};
}
What is the value of WeaponDamage
?
Consider the following program:
class Weapon {
public:
int GetDamage(){ return Damage; }
int Damage{10};
};
class Spear : public Weapon {
public:
int Damage;
};
int main(){
Spear IronSpear;
IronSpear.Damage = 20;
int WeaponDamage{IronSpear.GetDamage()};
}
What is the value of WeaponDamage
?
When working with inheritance, we often want to modify the behavior of one or more of our inherited functions.
Typically, this involves defining a function with the same prototype as the one we’re inheriting.
Below, we’ve provided a specific implementation of an inherited function, allowing us to implement subtype-specific behaviors:
#include <iostream>
using namespace std;
class Monster {
public:
void Attack(){ cout << "Monster Attacking"; }
};
class Goblin : public Monster {
public:
void Attack(){ cout << "Goblin Attacking"; }
};
int main(){
Goblin Bonker;
Bonker.Attack();
}
Goblin Attack
Similar to variables, this is another example of shadowing. The function we’re shadowing still exists on the base class. But with functions, this is rarely as problematic as it is for variables.
Later in the course, we’ll introduce the concept of polymorphism, which builds on this concept to create extremely flexible and intuitive designs.
Consider the following code:
class Weapon {
public:
int GetDamage(){ return Damage; }
protected:
int Damage{10};
};
class MagicalSword : public Weapon {
public:
int GetDamage(){
return isEnchanted ? Damage * 2 : Damage;
}
protected:
bool isEnchanted{true};
};
int main(){
MagicalSword SwordOfLight;
int WeaponDamage{SwordOfLight.GetDamage()};
}
What is the value of WeaponDamage
?
Expanding on the idea of providing a replacement implementation of an inherited function, we’ll also sometimes want to take a more subtle approach.
For example, the inherited function might be quite complex, which we’ve simulated below just with a load of logging:
#include <iostream>
using namespace std;
class Monster {
public:
void Attack(){
cout << "\nMonster Attacking";
cout << "\nPlaying Animation";
cout << "\nPlaying Sound";
cout << "\nUpdating UI";
cout << "\nEven More Stuff";
}
};
class Goblin : public Monster {
public:
void Attack(){
// ...??
}
};
int main() {
Goblin Bonker;
Bonker.Attack();
}
We may want the version of that function in our subclass to do all those things, too - we just want it to do something extra.
When shadowing an inherited function, we can call the inherited version using the base class name, and the scope resolution operator ::
It looks like this:
#include <iostream>
using namespace std;
class Monster {
public:
void Attack(){
cout << "\nMonster Attacking";
cout << "\nPlaying Animation";
cout << "\nPlaying Sound";
cout << "\nUpdating UI";
cout << "\nEven More Stuff";
}
};
class Goblin : public Monster {
public:
void Attack(){
Monster::Attack();
cout << "\n\nand now some Goblin things";
}
};
int main(){
Goblin Bonker;
Bonker.Attack();
}
Monster Attacking
Playing Animation
Playing Sound
Updating UI
Even More Stuff
and now some Goblin things
Super
(Unreal Engine)In quite a few programming languages, the ability to dynamically reference the parent class, ie, "superclass", is sometimes granted through the Super
or super
keyword:
void Attack(){
Super::Attack();
cout << "\n\nand now some Goblin things";
}
The C++ specification does not include this, but it is sometimes added by users. For example, the Super
keyword is available when working in the Unreal Engine ecosystem.
Consider the following code:
class Monster {
public:
void SetDamage(int Damage){
mDamage = Damage;
}
int GetDamage(){ return mDamage; }
private:
int mDamage;
};
class Goblin : public Monster {
public:
void SetDamage(int Damage){
Monster::SetDamage(
isEnraged ? Damage * 2 : Damage);
}
private:
bool isEnraged{true};
};
int main(){
Goblin Bonker;
Bonker.SetDamage(15);
int GoblinDamage{Bonker.GetDamage()};
}
What will be the value of GoblinDamage
?
In this lesson, we dove deep into the nuances of using inherited members in C++. We learned how to call inherited constructors from the member initializer list of derived constructors, ensuring a smooth and efficient setup of our derived objects. Key highlights from this lesson include:
public
or protected
).This lesson provides an in-depth exploration of using inherited methods and variables in C++, covering constructor calls, variable modification, and function shadowing
Become a software engineer with C++. Starting from the basics, we guide you step by step along the way