Understanding Inheritance in C++
In object-oriented programming, inheritance allows a class to inherit properties and behaviors from another class. The class that inherits is called the derived class (or subclas), while the class being inherited from is called the base class (or superclass). This article explores the fundamentals of inheritance in C++, covering syntax, access control, constructors, and function overloading.
Basic Inheritance Syntax
The simplest form of inheritance uses the public keyword to specify how base class members are accessible in the derived class. When a derived class object is created, the base class constructor executes first, followed by the derived class constructor. Upon destruction, the order reverses: derived class destructor runs first, then base class destructor.
enum class StudentStatus {
ACTIVE,
GRADUATED,
SUSPENDED
};
class UniversityMember
{
private:
std::string fullName;
StudentStatus status;
int yearsEnrolled;
public:
UniversityMember() : fullName(""), status(StudentStatus::ACTIVE), yearsEnrolled(0)
{
std::cout << "UniversityMember::UniversityMember()" << std::endl;
}
~UniversityMember()
{
std::cout << "UniversityMember::~UniversityMember()" << std::endl;
}
};
class Instructor : public UniversityMember
{
private:
int employeeId;
double salary;
public:
Instructor() : employeeId(0), salary(0.0)
{
std::cout << "Instructor::Instructor()" << std::endl;
}
~Instructor()
{
std::cout << "Instructor::~Instructor()" << std::endl;
}
};
When testing this code:
void TestBasicInheritance()
{
Instructor inst;
}
The execution order is: base class constructor → derived class constructor → derived class destructor → base class destructor.
Access Specifiers in Inheritance
Public Inheritance
With public inheritance, all public members of the base class become public members of the derived class, and protected members become protected. The derived class can access and manipulate these members directly.
enum class EmploymentType {
FULL_TIME,
PART_TIME,
CONTRACT
};
class Employee
{
private:
std::string employeeName;
EmploymentType type;
public:
Employee() : employeeName(""), type(EmploymentType::FULL_TIME), yearsOfService(0)
{
std::cout << "Employee::Employee()" << std::endl;
}
~Employee()
{
std::cout << "Employee::~Employee()" << std::endl;
}
void SetName(const std::string& name)
{
employeeName = name;
}
std::string GetName() const
{
return employeeName;
}
void SetEmploymentType(const EmploymentType empType)
{
type = empType;
}
EmploymentType GetEmploymentType() const
{
return type;
}
int yearsOfService;
void SetYearsOfService(int years)
{
yearsOfService = years;
}
int GetYearsOfService() const
{
return yearsOfService;
}
};
class Manager : public Employee
{
private:
int departmentCode;
int teamSize;
public:
Manager() : departmentCode(0), teamSize(0)
{
std::cout << "Manager::Manager()" << std::endl;
}
~Manager()
{
std::cout << "Manager::~Manager()" << std::endl;
}
void SetDepartmentCode(int code)
{
departmentCode = code;
}
int GetDepartmentCode() const
{
return departmentCode;
}
void SetTeamSize(int size)
{
teamSize = size;
}
int GetTeamSize() const
{
return teamSize;
}
};
void TestPublicAccess()
{
Manager mgr;
mgr.SetName("Alice");
mgr.SetEmploymentType(EmploymentType::FULL_TIME);
mgr.yearsOfService = 5; // Direct access to public member
mgr.SetDepartmentCode(101);
mgr.SetTeamSize(10);
std::cout << "Name\t" << "Type\t" << "Years\t" << "Dept\t" << "Team" << std::endl;
std::cout << mgr.GetName() << "\t" << static_cast<int>(mgr.GetEmploymentType())
<< "\t" << mgr.yearsOfService << "\t" << mgr.GetDepartmentCode()
<< "\t" << mgr.GetTeamSize() << std::endl;
}</int>
This compiles and runs successfully.
Private Inheritance
With private inheritance, all base class members become private in the derived class. The derived class retains access to these members through base class public methods, but external code cannot access them directly through a derived class object.
enum class VehicleType {
CAR,
TRUCK,
MOTORCYCLE
};
class Vehicle
{
private:
std::string modelName;
VehicleType type;
int mileage;
public:
Vehicle() : modelName(""), type(VehicleType::CAR), mileage(0)
{
std::cout << "Vehicle::Vehicle()" << std::endl;
}
~Vehicle()
{
std::cout << "Vehicle::~Vehicle()" << std::endl;
}
void SetModel(const std::string& model)
{
modelName = model;
}
std::string GetModel() const
{
return modelName;
}
void SetVehicleType(const VehicleType vt)
{
type = vt;
}
VehicleType GetVehicleType() const
{
return type;
}
};
class SportsCar : private Vehicle
{
private:
int horsepower;
double zeroToSixty;
public:
SportsCar() : horsepower(0), zeroToSixty(0.0)
{
std::cout << "SportsCar::SportsCar()" << std::endl;
}
~SportsCar()
{
std::cout << "SportsCar::~SportsCar()" << std::endl;
}
void SetHorsepower(int hp)
{
horsepower = hp;
}
int GetHorsepower() const
{
return horsepower;
}
void SetAcceleration(double time)
{
zeroToSixty = time;
}
double GetAcceleration() const
{
return zeroToSixty;
}
};
void TestPrivateAccess()
{
SportsCar car;
car.SetModel("Ferrari");
car.SetVehicleType(VehicleType::CAR);
// car.mileage = 100; // Compilation error - mileage is private in SportsCar
car.SetHorsepower(700);
car.SetAcceleration(2.8);
}
Attempting to access base class members directly through the derived class object results in a compilation error.
Protected Inheritance
With protected inheritance, public and protected members of the base class become protected in the derived class. These members are accessible within the derived class and its subclasses, but not through external objects.
enum class AccountType {
CHECKING,
SAVINGS,
INVESTMENT
};
class BankAccount
{
private:
std::string accountHolder;
AccountType accountType;
double balance;
protected:
void DisplayAccountDetails()
{
std::cout << "BankAccount::DisplayAccountDetails()\t";
std::cout << "Holder: " << accountHolder << "\tType: " << static_cast<int>(accountType)
<< "\tBalance: " << balance << std::endl;
}
public:
BankAccount() : accountHolder(""), accountType(AccountType::CHECKING), balance(0.0)
{
std::cout << "BankAccount::BankAccount()" << std::endl;
}
~BankAccount()
{
std::cout << "BankAccount::~BankAccount()" << std::endl;
}
void SetHolder(const std::string& holder)
{
accountHolder = holder;
}
std::string GetHolder() const
{
return accountHolder;
}
void SetAccountType(const AccountType at)
{
accountType = at;
}
AccountType GetAccountType() const
{
return accountType;
}
void SetBalance(double amt)
{
balance = amt;
}
double GetBalance() const
{
return balance;
}
};
class SavingsAccount : protected BankAccount
{
private:
double interestRate;
int withdrawalLimit;
public:
SavingsAccount() : interestRate(0.0), withdrawalLimit(0)
{
std::cout << "SavingsAccount::SavingsAccount()" << std::endl;
}
~SavingsAccount()
{
std::cout << "SavingsAccount::~SavingsAccount()" << std::endl;
}
void SetInterestRate(double rate)
{
interestRate = rate;
}
double GetInterestRate() const
{
return interestRate;
}
void SetWithdrawalLimit(int limit)
{
withdrawalLimit = limit;
}
int GetWithdrawalLimit() const
{
return withdrawalLimit;
}
void ShowAccount()
{
DisplayAccountDetails(); // Protected method accessible within derived class
}
};</int>
void TestProtectedAccess()
{
BankAccount account;
// account.DisplayAccountDetails(); // Error - protected method not accessible
SavingsAccount savings;
savings.SetHolder("Bob");
savings.SetAccountType(AccountType::SAVINGS);
savings.SetBalance(5000.0);
savings.SetInterestRate(0.03);
savings.SetWithdrawalLimit(6);
savings.ShowAccount();
std::cout << "Holder\t" << "Type\t" << "Balance\t" << "Rate\t" << "Limit" << std::endl;
std::cout << savings.GetHolder() << "\t" << static_cast<int>(savings.GetAccountType())
<< "\t" << savings.GetBalance() << "\t" << savings.GetInterestRate()
<< "\t" << savings.GetWithdrawalLimit() << std::endl;
}</int>
Constructors and Destructors in Inheritance
When a derived class is initialized, the base class must be constructed first. The derived class constructor can initialize the base class only through the member initilaizer list—there is no other way to pass arguments to the base class constructor.
enum class Qualification {
BACHELORS,
MASTERS,
DOCTORATE
};
class Researcher
{
private:
std::string researcherName;
Qualification highestQual;
int publications;
public:
Researcher(const std::string& name, const Qualification qual, const int pubs)
: researcherName(name), highestQual(qual), publications(pubs)
{
std::cout << "Researcher::Researcher()" << std::endl;
}
~Researcher()
{
std::cout << "Researcher::~Researcher()" << std::endl;
}
};
class Professor : public Researcher
{
private:
int employeeId;
int coursesTaught;
public:
Professor(const int id, const int courses)
: Researcher("", Qualification::MASTERS, 0), employeeId(id), coursesTaught(courses)
{
std::cout << "Professor::Professor()" << std::endl;
}
~Professor()
{
std::cout << "Professor::~Professor()" << std::endl;
}
};
Function Overloading and Inheritance
Overloading Basics
Function overloading occurs when multiple functions within the same class share the same name but differ in their parameter lists. The compiler determines which function to call based on the arguments provided.
enum class Priority {
LOW,
MEDIUM,
HIGH
};
class Task
{
private:
std::string taskName;
Priority priority;
int estimatedHours;
public:
Task(const std::string& name, const Priority pri, const int hours)
: taskName(name), priority(pri), estimatedHours(hours)
{
std::cout << "Task::Task()" << std::endl;
}
~Task()
{
std::cout << "Task::~Task()" << std::endl;
}
void Display()
{
std::cout << "Task::Display - No parameters" << std::endl;
}
void Display(const int detailLevel)
{
std::cout << "Task::Display - Detail level: " << detailLevel << std::endl;
}
void Display(const std::string& format)
{
std::cout << "Task::Display - Format: " << format << std::endl;
}
void Display(const int priority, const int hours)
{
std::cout << "Task::Display - Priority: " << priority << ", Hours: " << hours << std::endl;
}
};
void TestOverloading()
{
Task task("Implementation", Priority::HIGH, 40);
task.Display();
task.Display(2);
task.Display("summary");
task.Display(1, 40);
}
Overloading with Inheritance
Case 1: No Overridden Functions in Derived Class
When the derived class does not define any function with the same name as base class overloaded functions, the derived class objects can still call all base class overloads based on argument types.
enum class Department {
ENGINEERING,
MARKETING,
SALES
};
class Staff
{
private:
std::string staffName;
Department dept;
int performanceScore;
public:
Staff(const std::string& name, const Department d, const int score)
: staffName(name), dept(d), performanceScore(score)
{
std::cout << "Staff::Staff()" << std::endl;
}
~Staff()
{
std::cout << "Staff::~Staff()" << std::endl;
}
void ShowInfo()
{
std::cout << "Staff::ShowInfo - Default" << std::endl;
}
void ShowInfo(const int verbose)
{
std::cout << "Staff::ShowInfo - Verbose: " << verbose << std::endl;
}
void ShowInfo(const std::string& format)
{
std::cout << "Staff::ShowInfo - Format: " << format << std::endl;
}
void ShowInfo(const int detail, const int indent)
{
std::cout << "Staff::ShowInfo - Detail: " << detail << ", Indent: " << indent << std::endl;
}
};
class TeamLead : public Staff
{
private:
int teamId;
int teamMemberCount;
public:
TeamLead(const int id, const int count)
: Staff("", Department::ENGINEERING, 0), teamId(id), teamMemberCount(count)
{
std::cout << "TeamLead::TeamLead()" << std::endl;
}
~TeamLead()
{
std::cout << "TeamLead::~TeamLead()" << std::endl;
}
};
void TestInheritedOverloads()
{
TeamLead lead(2001, 8);
lead.ShowInfo();
lead.ShowInfo(1);
lead.ShowInfo("detailed");
lead.ShowInfo(3, 2);
}
Case 2: Function Name Hiding
If the derived class defines any function with the same name as a base class function (regardless of parameters), C++ performs name hiding. All base class functtions with that name become hidden, and only the derived class version is accessible.
enum class PlayerLevel {
BEGINNER,
INTERMEDIATE,
ADVANCED,
EXPERT
};
class GamePlayer
{
private:
std::string playerName;
PlayerLevel level;
int experience;
public:
GamePlayer(const std::string& name, const PlayerLevel lv, const int xp)
: playerName(name), level(lv), experience(xp)
{
std::cout << "GamePlayer::GamePlayer()" << std::endl;
}
~GamePlayer()
{
std::cout << "GamePlayer::~GamePlayer()" << std::endl;
}
void LevelUp()
{
std::cout << "GamePlayer::LevelUp - Default" << std::endl;
}
void LevelUp(const int stages)
{
std::cout << "GamePlayer::LevelUp - Stages: " << stages << std::endl;
}
void LevelUp(const std::string& bonus)
{
std::cout << "GamePlayer::LevelUp - Bonus: " << bonus << std::endl;
}
void LevelUp(const int stages, const std::string& reward)
{
std::cout << "GamePlayer::LevelUp - Stages: " << stages << ", Reward: " << reward << std::endl;
}
};
class VIPPlayer : public GamePlayer
{
private:
int vipPoints;
std::string vipTier;
public:
VIPPlayer(const std::string& name, const PlayerLevel lv, const int xp)
: GamePlayer(name, lv, xp), vipPoints(0), vipTier("Bronze")
{
std::cout << "VIPPlayer::VIPPlayer()" << std::endl;
}
~VIPPlayer()
{
std::cout << "VIPPlayer::~VIPPlayer()" << std::endl;
}
void LevelUp()
{
std::cout << "VIPPlayer::LevelUp - VIP Enhanced" << std::endl;
}
};
void TestNameHiding()
{
VIPPlayer vip("Champion", PlayerLevel::ADVANCED, 5000);
vip.LevelUp();
// vip.LevelUp(3); // Compilation error - hidden
// vip.LevelUp("gold"); // Compilation error - hidden
// vip.LevelUp(2, "item"); // Compilation error - hidden
}
This behavior, unique to C++ among object-oriented languages, is called name hiding. It ensures that derived class functions always take precedence over base class functions with identical names.