Working with Inherited Members

This lesson provides an in-depth exploration of using inherited methods and variables in C++, covering constructor calls, variable modification, and function shadowing
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

Free, Unlimited Access
3D art of a man using a telescope
Ryan McCombe
Ryan McCombe
Updated

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.

Calling Inherited Constructors

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
Test your Knowledge

Inherited Constructors

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?

Updating Inherited Variables

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

Constructor Call Order

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

Common Mistake: Shadowing Inherited Variables

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.

Test your Knowledge

Inherited Variables

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?

Shadowing Inherited Functions

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.

Test your Knowledge

Shadowing Inherited Functions

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?

Extending Inherited Functions

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

Using 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.

Test your Knowledge

Extending Inherited Functions

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?

Summary

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:

  • Understanding the order and process of calling base class constructors in derived classes.
  • Learning to modify inherited variables in derived classes and the importance of variable visibility (public or protected).
  • Recognizing and avoiding common pitfalls, such as variable shadowing, which can lead to confusing bugs.
  • Exploring how to shadow and extend inherited functions, allowing for more specialized behaviors in derived classes.

Was this lesson useful?

Next Lesson

References

This lesson demystifies references, explaining how they work, their benefits, and their role in performance and data manipulation
3D art showing a female blacksmith character
Ryan McCombe
Ryan McCombe
Updated
Lesson Contents

Working with Inherited Members

This lesson provides an in-depth exploration of using inherited methods and variables in C++, covering constructor calls, variable modification, and function shadowing

3D art showing a progammer setting up a development environment
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

Free, Unlimited Access
Inheritance
3D art showing a progammer setting up a development environment
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

Free, unlimited access

This course includes:

  • 59 Lessons
  • Over 200 Quiz Questions
  • 95% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

References

This lesson demystifies references, explaining how they work, their benefits, and their role in performance and data manipulation
3D art showing a female blacksmith character
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved