Understanding Inheritance in C++
Inheritance serves as the most crucial mechanism in object-oriented programming for code reuse, allowing developers to extend existing classes while preserving their original characteristics. This approach creates new classes known as derived classes, establishing a hierarchical structure that mirrors the cognitive process of moving from simple to complex concepts. While function-level reuse is common, inheritance provides reuse at the class design level.
In any inheritance relationship, derived classes must exhibit characteristics that distinguish them from their base classes. Beyond inheriting base class members, derived classes typically extend their functionality by adding new data members and methods.
Basic Inheritance Syntax
Consider the following example where we define a base class and create derived classes from it:
class Individual {
public:
void DisplayInfo() {
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "default_name";
int _age = 25;
};
class Learner : public Individual {
protected:
int _studentID;
};
class Educator : public Individual {
protected:
int _employeeID;
};
int main() {
Learner l;
Educator e;
l.DisplayInfo();
e.DisplayInfo();
return 0;
}
Inheritance Types and Access Control
When using inheritance, access specifiers control how base class members are inherited:
- private members of the base class are never directly accessible in derived classes, regardless of inheritance type
- protected members were introduced specifically for inheritance, allowing access within derived classes
- The access level of inherited members equals the minimum of the original access specifier and the inheritance type
- When using
class, default inheritance isprivate; when usingstruct, default inheritance ispublic
Public inheritance is the most commonly used form, as protected and private inheritance limit usability and reduce maintainability.
class Individual {
public:
void Display() {
cout << _name << endl;
}
protected:
string _name;
private:
int _age;
};
class Learner : public Individual {
protected:
int _learnerNumber;
};
Base and Derived Class Assignment
Derived class objects can be assigned to base class objects, pointers, or references—a process sometimes called slicing. However, base class objects cannot be assigned to derived class objects.
class Individual {
protected:
string _name;
string _gender;
int _age;
};
class Learner : public Individual {
public:
int _studentID;
};
void Test() {
Learner learnerObj;
// 1. Derived objects can be assigned to base objects/pointers/references
Individual individualObj = learnerObj;
Individual* ptr = &learnerObj;
Individual& ref = learnerObj;
// 2. Base objects cannot be assigned to derived objects
// learnerObj = individualObj; // This would cause an error
}
Inheritance and Scope
Base classes and derived classes have separate scopes. When both define members with the same name, the derived class member hides the base class member—a phenomenon known as hiding or redefinition. The base class member can still be accessed using the scope resolution operator.
class Individual {
protected:
string _name = "John Doe";
int _ID = 12345;
};
class Learner : public Individual {
public:
void PrintInfo() {
cout << "Name: " << _name << endl;
// Explicitly access base class member
cout << "ID: " << Individual::_ID << endl;
// Default access to derived class member
cout << "Student ID: " << _ID << endl;
}
protected:
int _ID = 67890; // Hides base class _ID
};
Default Member Functions in Derived Classes
When deriving classes, the six default member functions behave as follows:
- Constructors must call base class constructors to initialize base class members
- Copy constructors must call base class copy constructors
- Assignment operators must call base class assignment operators
- Destructors automatically call base class destructors after executing their own code
- Object initialization follows base class construction before derived class construction
- Object destruction follows derived class destruction before base class destruction
class Individual {
public:
Individual(const char* name = "default")
: _name(name) {
cout << "Individual()" << endl;
}
Individual(const Individual& i)
: _name(i._name) {
cout << "Individual(const Individual&)" << endl;
}
Individual& operator=(const Individual& i) {
cout << "Individual operator=" << endl;
if (this != &i)
_name = i._name;
return *this;
}
~Individual() {
cout << "~Individual()" << endl;
}
protected:
string _name;
};
class Learner : public Individual {
public:
Learner(const char* name, int id)
: Individual(name)
, _studentID(id) {
cout << "Learner()" << endl;
}
Learner(const Learner& l)
: Individual(l)
, _studentID(l._studentID) {
cout << "Learner(const Learner&)" << endl;
}
Learner& operator=(const Learner& l) {
cout << "Learner operator=" << endl;
if (this != &l) {
Individual::operator=(l);
_studentID = l._studentID;
}
return *this;
}
~Learner() {
cout << "~Learner()" << endl;
}
protected:
int _studentID;
};
Friendship and Inheritance
Friendship relationships are not inherited. A friend of a base class cannot access private or protected members of derived classes.
class Learner;
class Individual {
public:
friend void Display(const Individual& i, const Learner& l);
protected:
string _name;
};
class Learner : public Individual {
protected:
int _studentNumber;
};
void Display(const Individual& i, const Learner& l) {
cout << i._name << endl;
// cout << l._studentNumber << endl; // Not accessible
}
Static Members in Inheritance
When a base class defines static members, there is only one instance of that member throughout the entire inheritance hierarchy, regardless of how many derived classes exist.
class Individual {
public:
Individual() { ++_count; }
protected:
string _name;
public:
static int _count; // Tracks number of individuals
};
int Individual::_count = 0;
class Learner : public Individual {
protected:
int _studentID;
};
class Graduate : public Learner {
protected:
string _researchTopic;
};
int main() {
Learner l1, l2, l3;
Graduate g1;
cout << "Total individuals: " << Individual::_count << endl; // 4
Learner::_count = 0;
cout << "Total individuals: " << Individual::_count << endl; // 0
return 0;
}
Diamond Inheritance and Virtual Inheritance
Single inheritance occurs when a derived class has only one direct base class. Multiple inheritance occurs when a derived class has two or more direct base classes. Diamond inheritance is a special case of multiple inheritance where two derived classes inherit from the same base class, and another class inherits from both derived classes.
Diamond inheritance introduces two main problems: data redundancy and ambiguity. Virtual inheritance can resolve these issues.
// Without virtual inheritance (problematic)
class Person {
public:
string _name;
};
class Student : public Person {
protected:
int _studentNumber;
};
class Teacher : public Person {
protected:
int _teacherID;
};
class TeachingAssistant : public Student, public Teacher {
protected:
string _majorSubject;
};
void Test() {
TeachingAssistant ta;
ta._name = "John"; // Ambiguity error
// Must specify which base class:
ta.Student::_name = "John";
ta.Teacher::_name = "John";
}
// With virtual inheritance (solution)
class Person {
public:
string _name;
};
class Student : virtual public Person {
protected:
int _studentNumber;
};
class Teacher : virtual public Person {
protected:
int _teacherID;
};
class TeachingAssistant : public Student, public Teacher {
protected:
string _majorSubject;
};
void Test() {
TeachingAssistant ta;
ta._name = "John"; // No ambiguity
}
Inheritance vs. Composition
Public inheritance represents an "is-a" relationship, where each derived class object is also a base class object. Composition represents a "has-a" relationship, where each containing class object has another object as a member.
Composition is generally preferred over inheritance because it:
- Results in lower coupling between classes
- Better maintains encapsulation
- Provides more flexible and maintainable code
However, inheritance is necessary for achieving polymorphism and is appropriate when an "is-a" relationship truly exists between classes.
// "is-a" relationship - inheritance
class Vehicle {
protected:
string _color = "White";
string _licensePlate = "ABC123";
};
class Motorcycle : public Vehicle {
public:
void Ride() { cout << "Fast and agile" << endl; }
};
// "has-a" relationship - composition
class Wheel {
protected:
string _brand = "Michelin";
int _diameter = 17;
};
class Car {
protected:
string _color = "White";
string _licensePlate = "ABC123";
Wheel _wheel; // Composition
};