5.2 Inheritance

Inheritance
Section titled “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.
Class Hierarchy
Section titled “Class Hierarchy”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().
Account ↔ CurrentAccount
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.
Account ↔ CurrentAccount
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;}🧩Knowledge Check
Section titled “🧩Knowledge Check”What is the primary purpose of inheritance in C++?
Initialising Base Classes
Static and Dynamic Types
Section titled “Static and Dynamic Types”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
- 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.
- 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.
- 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.
- 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.
- // 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.
- 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.
- 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 EuroDisplaying ptrB:Account number: 12345 has balance: 50 Euro And overdraft limit: 200Displaying ptrA again:Account number: 12345 has balance: 50 Euro And overdraft limit: 200Static vs Dynamic Types
What a pointer is declared as versus what it actually points to.
The Static vs Dynamic Types figure shows the following three cases:
- When the pointers
ptrAandptrBhave 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 anAccountpointer points at theAccountobject and theCurrentAccountpointer points at theCurrentAccountobject. - The pointer
ptrAhas static typeAccountand dynamic typeCurrentAccount. This illustrates the state of the application after the 6th step takes place. TheAccountpointer is assigned to theCurrentAccountpointer and this is allowed. To understand this, you can think of theAccountpointer as understanding thedisplay(),makeLodgement()andmakeWithdrawal()methods. So, we can call any of these methods on theptrAand the object that we are calling these methods on understands all these methods (and more!). This is guaranteed in this case! Why? TheCurrentAccountclass is a child of theAccountclass and so has inherited all the methods fromAccount. Therefore, calling the methods of the pointer to theAccount ptrAworks perfectly. - Pointer
ptrBhas static typeCurrentAccountand dynamic typeAccountThis example is INVALID. It shows the assignment ofptrB = ptrA;which is not allowed. If this case was allowed then theCurrentAccountpointerptrBwould be pointing to anAccountobject. But theCurrentAccountpointer “understands” the methodsdisplay(),makeLodgement(),makeWithdrawal()andsetOverdraftLimit(). So what would happen if thesetOverdraftLimit()was called on theAccountobjecta? Well, it does not have asetOverdraftLimit()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.
🧩 Knowledge Check
Section titled “🧩 Knowledge Check”What causes 'Object Slicing' to occur in a C++ application?
Match Polymorphism Concepts
According to the rules of C++ polymorphism, which of the following pointer assignments is valid?
Using the Scope Resolution Operator
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> //1using 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
- The
<string>header file must be included to allow the use of strings within the application. - 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 thegetAccountType()method. These are known as pure virtual functions. - 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 thegetAccountType()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
- 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. - 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. - The
Account::display()method is called in the same way as before, so thedisplay()method of the parent is used, which in turn calls thegetAccountType()method of the child class. - The call to
b.display()calls the display method ofCurrentAccount, which in turn calls thedisplay()method of the parent Account class.
🧩Knowledge Check
Section titled “🧩Knowledge Check”Match Abstract Class Terminology
What happens if you attempt to create an object directly from an abstract class in C++?
The override and final Keywords (C++11)
Section titled “The override and final Keywords (C++11)”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.
🧩Knowledge Check
Section titled “🧩Knowledge Check”What is the primary benefit of using the 'override' keyword in C++11 and later?
How does the 'final' keyword affect inheritance and method overriding in C++?
© 2026 Derek Molloy, Dublin City University. All rights reserved.