Skip to content

Search is only available in production builds. Try building and previewing the site to test it out locally.

5.2 Inheritance

In an object-oriented language:

  • The compiler must understand the class hierarchy.
  • If we extend a class, the compiler must combine the extension with the definition of the base class.

In a non-object-oriented language:

  • The user must rely on the programmer documentation.
  • The user must define each class from scratch.

The best way to explain inheritance is to begin with a practical application. We will develop a new CurrentAccount class that extends the functionality of the Account class to add overdraft facilities (i.e., that you can have a negative account balance.)

One Solution: Define a new class CurrentAccount that inherits the functionality of Account, so, Account is the base class of CurrentAccount and equivalently, CurrentAccount is the derived class of Account (i.e., CurrentAccount is-a Account).

What additional functionality should the CurrentAccount class have? Well, a current account is usually a non-interest-earning account that has overdraft facilities, so we could add:

  • An overdraft limit (float).
  • A facility to set this limit (e.g., setOverdraftLimit())

So, this can be visualised as Figure 2, where an overdraftLimit state and a setOverdraftLimit() method have been added to the CurrentAccount class description. The class also inherits the states and methods of the Account class, e.g., balance, accountNumber, display(), makeLodgement() and makeWithdrawal().

UML Diagram

Account ↔ CurrentAccount

Account
- balance : float
+ display() : void
+ makeLodgement(float amount) : void
+ makeWithdrawal(float amount) : void
CurrentAccount

Figure 2. Account with CurrentAccount subclass (Intermediate).

Hover or tap any member for a note.

So, inheritance allows a class to be inherited and extended with new functionality. A problem still remains in that the display() method that we have inherited does not work sufficiently for the CurrentAccount class. The display() method only displays the balance and accountNumber state details — it does not display the overdraftLimit state.

UML Diagram

Account ↔ CurrentAccount

Account
- balance : float
+ display() : void
+ makeLodgement(float amount) : void
+ makeWithdrawal(float amount) : void
CurrentAccount

Figure 3. Account with CurrentAccount subclass (Final).

Hover or tap any member for a note.

So, we need to replace the behaviour (or override the behaviour) of the display() method with an updated method that also displays the overdraftLimit details. To override the display method, it can simply be re-defined in the CurrentAccount class. This discussion is also relevant for the makeWithdrawal() method, as it will have to behave slightly differently when a negative balance is reached. This new inheritance structure is illustrated in Figure 3.

So, inheritance allows a derived class to extend its parent with newly inherited functionality, and also allows functionality to be modified.

The definition of this new CurrentAccount is as follows:

class CurrentAccount: public Account {
float overdraftLimit;
public:
CurrentAccount(float bal, int actNum, float limit);
virtual void setOverdraftLimit(float newLimit);
virtual void display();
virtual void makeWithdrawal(float amount);
};

A full source code example for this is in currentAccount.cpp. The implementation/definition of these methods is as follows.

CurrentAccount::CurrentAccount(float bal, int actNum, float limit):
Account(bal, actNum), overdraftLimit(limit) {}
void CurrentAccount::display() {
cout << "Account number: " << accountNumber
<< " has balance: " << balance << " Euro" << "\n";
cout << " And overdraft limit: " << overdraftLimit << "\n";
}
void CurrentAccount::makeWithdrawal(float amount) {
if (amount < (balance + overdraftLimit)) {
balance = balance - amount;
}
}
void CurrentAccount::setOverdraftLimit(float limit) {
overdraftLimit = limit;
}
Quiz
Select 0/1

What is the primary purpose of inheritance in C++?

Code Cloze
C++

Initialising Base Classes

The static type of a variable is its declared type at compile time. The dynamic type is the actual type of the object the variable refers to at run time. In C++, these two types can differ when using pointers or references in a class hierarchy, a core concept of polymorphism.

Let’s review the code and its explanation (See currentAccount2.cpp):

int main() {
Account a = Account(35.00, 34234324);
Account *ptrA = &a; //1
CurrentAccount b = CurrentAccount(50.0, 12345, 200.0);
CurrentAccount *ptrB = &b; //2
cout << "Displaying ptrA:" << "\n";
ptrA->display(); //3
cout << "Displaying ptrB:" << "\n";
ptrB->display(); //4
//ptrB = ptrA; // not allowed //5
ptrA = ptrB; //6
cout << "Displaying ptrA again:" << "\n";
ptrA->display(); //7
}

Analysis of this Code

  1. Account *ptrA = &a; [Static type of ptrA: Account *][Dynamic type of *ptrA: Account] the Account pointer ptrA points to an Account object. Both static and dynamic types are the same.
  2. CurrentAccount *ptrB = &b; [Static type of ptrB: CurrentAccount *][Dynamic type of *ptrB: CurrentAccount] The CurrentAccount pointer ptrB points to a CurrentAccount object. Both static and dynamic types are the same.
  3. ptrA->display(); This calls the display() method on the Account object. Because the dynamic type of *ptrA is Account, the Account::display() method is executed.
  4. ptrB->display(); This calls the display() method on the CurrentAccount object. The dynamic type of *ptrB is CurrentAccount, so the CurrentAccount::display() method is executed.
  5. // ptrB = ptrA; // not allowed. It’s not allowed because you cannot implicitly convert a pointer to a base class (Account *) to a pointer to a derived class (CurrentAccount *). This prevents a derived-class pointer from referring to a base-class object that doesn’t have all the members of the derived class.
  6. ptrA = ptrB; This is the key to polymorphism. You can implicitly convert a pointer to a derived class to a pointer to a base class. This is because a CurrentAccount “is-a” Account. The static type of ptrA is still Account *, but now its dynamic type is CurrentAccount.
  7. ptrA->display(); The ptrA Account’s display() method is called, which results in the display() method of the CurrentAccount object being called (in this case). This is accurate only if the display() method is a virtual function in the Account class (We will discuss this when we deal with virtual and non-virtual functions).

The output from this code segment is shown below:

Displaying ptrA:
account number: 34234324 has balance: 35 Euro
Displaying ptrB:
Account number: 12345 has balance: 50 Euro
And overdraft limit: 200
Displaying ptrA again:
Account number: 12345 has balance: 50 Euro
And overdraft limit: 200
Reference figure

Static vs Dynamic Types

What a pointer is declared as versus what it actually points to.

Memory
Account A object
- accountNumber : 3423424
- balance : 35.00
+ display() : void
+ makeLodgement(float amount) : void
+ makeWithdrawal(float amount) : void
CurrentAccount B object
- overdraftLimit : 200.00
- accountNumber : 12345
- balance : 50.00
+ setOverdraftLimit(float amount) : void
+ display() : void
+ makeWithdrawal(float amount) : void
+ makeLodgement(float amount) : void
ptrAAccount *
Understands the methods:
+ display() : void
+ makeLodgement(float amount) : void
+ makeWithdrawal(float amount) : void
ptrBCurrentAccount *
Understands the methods:
+ setOverdraftLimit(float amount) : void
+ display() : void
+ makeWithdrawal(float amount) : void
+ makeLodgement(float amount) : void
Initial state
  • Each pointer points at an object whose actual class matches its static (declared) type.
  • ptrA can call display(), makeLodgement(), makeWithdrawal(). ptrB additionally understands setOverdraftLimit().

The pointer's static type (chip on its name bar) fixes the menu of methods you may call through it, even when the object on the other end of the arrow is something more capable.

The Static vs Dynamic Types figure shows the following three cases:

  1. When the pointers ptrA and ptrB have the same static and dynamic types. This illustrates the code segment after steps 1 and 2 have taken place. This means that the two objects are created in memory and the two pointers point at their respective objects, so an Account pointer points at the Account object and the CurrentAccount pointer points at the CurrentAccount object.
  2. The pointer ptrA has static type Account and dynamic type CurrentAccount. This illustrates the state of the application after the 6th step takes place. The Account pointer is assigned to the CurrentAccount pointer and this is allowed. To understand this, you can think of the Account pointer as understanding the display(), makeLodgement() and makeWithdrawal() methods. So, we can call any of these methods on the ptrA and the object that we are calling these methods on understands all these methods (and more!). This is guaranteed in this case! Why? The CurrentAccount class is a child of the Account class and so has inherited all the methods from Account. Therefore, calling the methods of the pointer to the Account ptrA works perfectly.
  3. Pointer ptrB has static type CurrentAccount and dynamic type Account This example is INVALID. It shows the assignment of ptrB = ptrA; which is not allowed. If this case was allowed then the CurrentAccount pointer ptrB would be pointing to an Account object. But the CurrentAccount pointer “understands” the methods display(), makeLodgement(), makeWithdrawal() and setOverdraftLimit(). So what would happen if the setOverdraftLimit() was called on the Account object a? Well, it does not have a setOverdraftLimit() method and so it would fail.

One very important point to note in the code segment above is that when the pointer ptrA of static type Account points to a CurrentAccount object and its display() method is called, it is the display() method of CurrentAccount that executes, not the Account version. This is clear in the output shown previously, where the overdraft limit is displayed the second time ptrA->display() is called. In other words, the method is executed on the dynamic type of the object, not the static type.

The ability of a variable to change its dynamic type during execution is a useful property of C++. It makes programs easier to extend and debug, but it also has consequences. Consider the previous example: the base class Account and the derived class CurrentAccount each provide their own implementation of the display() method. Which method is called depends on the dynamic type of the object. This feature is known as dynamic binding. With dynamic binding, CurrentAccount::display() is called; without it, Account::display() would always be used.

The notation used above to distinguish between the two display() methods provides a clear indication of which method is being referenced—either Account::display() or CurrentAccount::display(). The :: symbol is a genuine operator in C++ known as the scope resolution operator.

The scope resolution operator is extremely important. For example, the display() method of CurrentAccount shows the balance, account number, and overdraft limit. However, the code used to display the balance and account number is identical to that in the parent class Account::display() method. Replicating code in this way is considered poor practice, as any future change to the base implementation would need to be repeated across all derived classes.

This issue can be avoided by using the scope resolution operator. For example, the display() method of CurrentAccount can be written as:

void CurrentAccount::display() {
Account::display();
cout << " And overdraft limit: " << overdraftLimit << "\n";
}

Here, the code from Account::display() is not duplicated by cut-and-paste; it is reused directly. Why is this important?

  • It allows the overridden method (Account::display()) to be called from within the new method (CurrentAccount::display()).
  • Code does not have to be rewritten.
  • If future modifications are made to the base class (e.g., adding an owner name or sort code), derived class methods will automatically incorporate them.
  • If a bug is found, it only needs to be fixed once.
  • Code is far easier to maintain.

In this example, only two lines of code are replicated. But imagine if it were hundreds of lines across ten different derived classes—this feature would be essential.

Polymorphism with References and Object Slicing

Section titled “Polymorphism with References and Object Slicing”

While the previous examples demonstrated polymorphism using pointers, C++ also supports polymorphism through references. Passing objects by reference is often the preferred and safer way to write polymorphic functions because references, unlike pointers, cannot be null.

Example: Polymorphism via References

void printAccountDetails(const Account& acc) {
// Dynamic binding works with references too!
acc.display();
}
int main() {
CurrentAccount myCurrent(50.0, 12345, 200.0);
printAccountDetails(myCurrent); // Safe: CurrentAccount::display() is called
return 0;
}

However, a very common and dangerous mistake is to pass the object by value instead of by reference or pointer. This leads to a problem known as Object Slicing.

Object Slicing
If you pass a derived class object by value to a base class parameter (or assign a derived object to a base variable), the compiler uses the base class’s copy constructor. It copies only the base class parts (e.g., accountNumber and balance) and completely discards (or “slices” off) the derived class parts (e.g., overdraftLimit).

// DANGER: Passed by value!
void printAccountBad(Account acc) {
acc.display(); // Will ALWAYS call Account::display(), never CurrentAccount::display()
}
int main() {
CurrentAccount myCurrent(50.0, 12345, 200.0);
printAccountBad(myCurrent); // The object is sliced!
return 0;
}

When printAccountBad is called, a new Account object is created on the stack. The CurrentAccount-specific data is lost, and because the new object is strictly an Account, dynamic binding does not happen. Always pass polymorphic objects by pointer or reference to avoid object slicing.

Quiz
Select 0/1

What causes 'Object Slicing' to occur in a C++ application?

Concept Match

Match Polymorphism Concepts

Quiz
Select 0/1

According to the rules of C++ polymorphism, which of the following pointer assignments is valid?

Code Cloze
C++

Using the Scope Resolution Operator

Quiz
Select 0/1

Why is it considered good practice to use the scope resolution operator to call a base class method from an overridden derived class method?

Abstract Classes
We discussed abstract classes earlier in the section on general Object-Oriented Programming. In C++, an abstract class is a class that may contain state variables and member methods, but provides no implementation for at least one of those methods—making the class incomplete.

An abstract class cannot be instantiated directly — attempting to create an object of an abstract class is a compile-time error. Every concrete (non-abstract) class that inherits from it must provide implementations for all pure virtual methods before objects of that class can be created.

Using the same banking example as before, we can illustrate this with the Account class. Suppose Account is modified to include a new abstract method called getAccountType(), which returns a string representing the type of account. This enforces the rule that every child class of Account must provide its own implementation of getAccountType(). Moreover, the method can still be referenced in the Account class, even though it has no implementation there.

#include <iostream>
#include <string> //1
using namespace std;
class Account {
protected:
int accountNumber;
float balance;
public:
Account(float aBalance, int anAccountNumber);
virtual string getAccountType() = 0; //2 pure virtual
virtual void display();
virtual void makeLodgement(float);
virtual void makeWithdrawal(float);
};
Account::Account(float aBalance, int anAccNumber) {
accountNumber = anAccNumber;
balance = aBalance;
}
void Account::display() {
cout << "Account type: " << getAccountType() << "\n"; //3
cout << "account number: " << accountNumber
<< " has balance: " << balance << " Euro" << "\n";
}
// ...

The full source code for this example is in abstractCurrentAccount.cpp

  1. The <string> header file must be included to allow the use of strings within the application.
  2. The assignment of "= 0" allows the method to be defined as abstract. This means that no implementation will be present for that method, in this case the getAccountType() method. These are known as pure virtual functions.
  3. Even though there is no actual implementation for the getAccountType() method, it can still be used. Since the class has an abstract method, the entire class is abstract and so cannot be instantiated, so no objects can now be created in the Account class. To use this class, a child class must exist and it must implement the getAccountType() method before an object can be created of that class.
class CurrentAccount : public Account {
float overdraftLimit;
public:
CurrentAccount(float balance, int accountNumber, float limit);
virtual string getAccountType(); //1
virtual void setOverDraftLimit(float newLimit);
virtual void display();
virtual void makeWithdrawal(float amount);
};
CurrentAccount::CurrentAccount(float balance, int accountNumber, float limit)
: Account(balance, accountNumber), overdraftLimit(limit) {}
string CurrentAccount::getAccountType() {
return "Current Account"; //2
}
void CurrentAccount::display() {
Account::display(); //3
cout << " And overdraft limit: " << overdraftLimit << "\n";
}
// ...
int main() {
// Account a = Account(35.00, 34234324); // NOT ALLOWED
// Account* ptrA = &a;
CurrentAccount b(50.0, 12345, 200.0);
b.display(); //4
}

The full source code for this example is listed in abstractCurrentAccount.cpp

  1. The abstract method getAccountType() must be re-defined in the child class. Note that this time there is no " = 0", stating that there will be an implementation in this class for the defined method.
  2. The implementation is coded. For this class all that occurs is that the string "Current Account" is returned from the method. The Deposit account implementation would return "Deposit Account" etc.
  3. The Account::display() method is called in the same way as before, so the display() method of the parent is used, which in turn calls the getAccountType() method of the child class.
  4. The call to b.display() calls the display method of CurrentAccount, which in turn calls the display() method of the parent Account class.
Concept Match

Match Abstract Class Terminology

Quiz
Select 0/1

What happens if you attempt to create an object directly from an abstract class in C++?

When a derived class provides a new implementation of a base class virtual method, it is overriding that method. C++11 introduced two keywords that make this explicit and catch errors at compile time.

override tells the compiler that this method is intended to override a virtual method in a base class. If no matching virtual method exists in any base class — for example, because of a typo in the method signature — the compiler reports an error. Without override, the mismatch would silently create a new, unrelated method rather than the intended override.

class CurrentAccount : public Account {
public:
void display() override; // compiler verifies Account::display() is virtual
string getAccountType() override; // compiler verifies Account::getAccountType() is virtual
};

final prevents further overriding of a virtual method in derived classes. It can also be applied to an entire class to prevent inheritance from it.

class CurrentAccount : public Account {
public:
void display() override final; // no further derived class can override display()
};
class PremiumAccount final : public CurrentAccount {
// no class may inherit from PremiumAccount
};

Both keywords serve as documentation as well as compiler-enforced contracts: readers of the code know immediately which methods are part of the polymorphic interface and which are fixed.

Quiz
Select 0/1

What is the primary benefit of using the 'override' keyword in C++11 and later?

Quiz
Select 0/1

How does the 'final' keyword affect inheritance and method overriding in C++?