1. Concept and Definition of Inheritance
Inheritance is a fundamental mechanism in object-oriented programming (OOP) that enables code reuse. It allows a new class (derived class) to extend an existing class (base class) by adding new features. Inheritance reflects the hierarchical structure of OOP and represents a cognitive process from simple to complex. Prior to inheritance, we primarily reused functions; inheritance facilitates reuse at the class design level.
#include <iostream>
#include <string>
// Base class
class Person {
public:
void print() const {
std::cout << "name: " << name_ << std::endl;
std::cout << "age: " << age_ << std::endl;
}
protected:
std::string name_ = "bear";
int age_ = 20;
};
// Derived class
class Student : public Person {
protected:
int studentId_;
};
// Another derived class
class Teacher : public Person {
protected:
int jobId_;
};
int main() {
Student s;
Teacher t;
s.print();
t.print();
return 0;
}
In the above code, both Student and Teacher reuse the members of Person. After inheritance, all members (functions and variables) of the base class Person become part of the derived class. In a debugger watch window, you can see that each Student and Teacher object contains a Person subobject.

Student and Teacher are called derived classes (or subclasses), while Person is the base class (or superclass).
Inheritance Syntax
The syntax for inheritance is: class DerivedClass : access-specifier BaseClass.
Access Specifiers and Inheritance Modes
There are three access specifiers:
publicprotectedprivate
And three inheritance modes:
publicinheritanceprotectedinheritanceprivateinheritance
Changes in Member Accessibility after Inheritance
The accessibility of base class members in the derived class depends on the combination of the base member's acces specifier and the inheritance mode:
| Base Member / Inheritance Mode | public inheritance | protected inheritance | private inheritance |
|---|---|---|---|
Base public member |
Derived public |
Derived protected |
Derived private |
Base protected member |
Derived protected |
Derived protected |
Derived private |
Base private member |
Inaccessible | Inaccessible | Inaccessible |
For example, Student inherits from Person via public inheritance but cannot access Person's private members inside Student.
Default Inheritance Mode
- If no inheritance mode is specified,
classdefaults toprivateinheritance, whilestructdefaults topublicinheritance.
Key Points to Remember
- Base class
privatemembers are always inaccessible in derived classes, even though they are inherited. The derived class cannot access them either internally or externally due to syntax restrictions. - If a base class member needs to be accessible in derived classes but not outside, use
protected. Hence,protectedexists primarily for inheritance. - The permission levels are:
public>protected>private. - Explicitly specify the inheritance mode for clarity, rather than relying on defaults.
- In practice,
publicinheritance is the most common and recommended.protectedandprivateinheritance are rarely used because they restrict member access to within the derived class, reducing extensibility and maintainability.
2. Object Slicing and Assignment between Base and Derived
A derived class object can be assigned to a base class object, a base class pointer, or a base class reference. This is known as object slicing, where the base class portion of the derived object is "sliced" off and assigned to the base object.

Assigning a derived object to a base pointer:

Assigning a derived object to a base reference:

Example:
class Person {
protected:
std::string name_;
std::string sex_;
int age_;
};
class Student : public Person {
protected:
int studentId_;
};
int main() {
Student s;
Person p = s; // Derived object to base object
Person* ptr = &s; // Derived object to base pointer
Person& ref = s; // Derived object to base reference
}
However, a base class object cannot be directly assigned to a derived class object. A base class pointer can be explicitly cast to a derived class pointer, but this is safe only if the base pointer actually points to a derived object.
3. Name Hiding and Scope
- In an inheritance hierarchy, the base and derived classes each have their own scope.
- If a derived class defines a member with the same name as a base class member, the derived class member hides the base class member. This is called name hiding (or redefinition). To access the hidden base member, use the scope resolution operator:
BaseClass::member. - For member functions, name hiding is based solely on the function name, not the signature. Hence, a derived class function with the same name hides all base class functions with that name, even if the parameters differ.
- To avoid confusion, it's best not to define members with the same name in base and derived classes.
Example:
#include <iostream>
class Person {
protected:
int age_ = 100;
};
class Student : public Person {
public:
void print() {
std::cout << age_ << std::endl; // Accesses Student's age_ (20)
}
protected:
int age_ = 20;
};
int main() {
Student s;
s.print(); // Output: 20
return 0;
}
To access the base class age_, use the scope resolution operator:
void print() {
std::cout << Person::age_ << std::endl; // Accesses Person's age_ (100)
}
Then s.print() would output 100.
4. Default Member Functions in Derived Classes
A class has six default member functions (constructor, copy constructor, destructor, copy assignment operator, and in C++11, move constructor and move assignment operator). Derived classes have special behavior for these:
Consider a base class Person:
#include <iostream>
#include <string>
class Person {
public:
Person(const std::string& name = "bear") : name_(name) {
std::cout << "Person()" << std::endl;
}
Person(const Person& p) : name_(p.name_) {
std::cout << "Person(const Person& p)" << std::endl;
}
Person& operator=(const Person& p) {
std::cout << "Person& operator=(const Person& p)" << std::endl;
if (this != &p) {
name_ = p.name_;
}
return *this;
}
~Person() {
std::cout << "~Person()" << std::endl;
}
private:
std::string name_;
};
The derived class Student properly implements its default members as follows:
class Student : public Person {
public:
// Constructor: initializes base part and derived members
Student(const std::string& name, int id)
: Person(name) // Initialize base part
, id_(id) // Initialize derived member
{
std::cout << "Student()" << std::endl;
}
// Copy constructor: copies base part via base copy constructor
Student(const Student& s)
: Person(s) // Slices to pass to Person copy constructor
, id_(s.id_)
{
std::cout << "Student(const Student& s)" << std::endl;
}
// Copy assignment: assigns base part via base operator=
Student& operator=(const Student& s) {
if (this != &s) {
Person::operator=(s); // Assign base part
id_ = s.id_; // Assign derived part
}
return *this;
}
// Destructor: automatically calls base destructor after derived destructor
~Student() {
std::cout << "~Student()" << std::endl;
// Base destructor ~Person() called automatically
}
private:
int id_;
};
Summary of Behavior:
- The derived class constructor must call the base class constructor to initialize the base part. If the base class has no default constructor, the call must be explicit in the member initializer list.
- The derived copy constructor should call the base copy constructor.
- The derived copy assignment operator should call the base copy assignment operator.
- The derived destructor automatically invokes the base destructor after completing its own destruction, ensuring proper cleanup order: derived members first, then base members.
- Construction sequence: base constructor → derived constructer.
- Destruction sequence: derived destructor → base destructor.
- To support polymorphism later, destructors are often
virtual. When a destructor is notvirtual, the derived destructor hides the base destructor (since the compiler internally names both asdestructor()).
5. Inheritance and Friendship
Friendship is not inherited. A friend of a base class does not have special access to members of derived classes. For example:
#include <iostream>
#include <string>
class Person {
friend void display(const Person& p, const Student& s);
protected:
std::string name_;
};
class Student : public Person {
protected:
int id_;
};
void display(const Person& p, const Student& s) {
std::cout << p.name_ << std::endl; // OK: friend of Person
std::cout << s.id_ << std::endl; // Error: not a friend of Student
}
int main() {
Person p;
Student s;
display(p, s);
return 0;
}
To allow display access to Student's protected/private members, you must declare it as a friend within Student:
class Student : public Person {
friend void display(const Person& p, const Student& s);
protected:
int id_;
};
6. Inheritance and Static Members
If a base class defines a static member, there is only one instance of that member shared across the entire inheritance hierarchy. All derived classes access the same static member.
#include <iostream>
#include <string>
class Person {
public:
Person() { ++count_; }
Person(const Person&) { ++count_; }
static int count_;
};
int Person::count_ = 0;
class Student : public Person {
protected:
std::string studentId_;
};
class Graduate : public Person {
protected:
std::string cppCourse_;
};
int main() {
Student s1;
Student s2(s1);
Student s3;
Graduate s4;
std::cout << Person::count_ << std::endl; // Output: 4
std::cout << Student::count_ << std::endl; // Output: 4
return 0;
}
Both outputs are 4, confirming that count_ is shared. The addresses are identical:
std::cout << &Person::count_ << std::endl; // e.g., 0x00000004
std::cout << &Student::count_ << std::endl; // e.g., 0x00000004
7. Multiple Inheritance and the Diamond Problem
Types of Inheritance
- Single inheritance: A derived class has exactly one direct base class.

- Multiple inheritance: A derived class has two or more direct base classes.

- Diamond inheritance: A special case of multiple inheritance where a class inherits from two or more classes that share a common base class, forming a diamond shape.

For example, if Assistant inherits from both Student and Teacher, and both inherit from Person, Assistant will contain two copies of Person subobjects. This leads to data redundancy and ambiguity (diamond problem).

The ambiguity can be resolved by specifying which base class member to use (e.g., d.Student::Person::_a), but the data redundancy remains.
Virtual Inheritance Solution
To solve both problems, use virtual inheritance. By declaring the inheritance from the common base class as virtual, only one shared copy of that base class subobject exists.
#include <iostream>
class A {
public:
int a_;
};
class B : virtual public A {
public:
int b_;
};
class C : virtual public A {
public:
int c_;
};
class D : public B, public C {
public:
int d_;
};
int main() {
D d;
d.B::a_ = 1;
d.C::a_ = 2;
d.b_ = 3;
d.c_ = 4;
d.d_ = 5;
return 0;
}
Now both B::a_ and C::a_ refer to the same A member, resolving ambiguity and redundancy.
How Virtual Inheritance Works Internally
The memory layout of a D object with virtual inheritance shows that the A subobject is stored at the end, and each B and C subobject contains a pointer to a virtual base table (vbtable). This pointer points to a table that holds an offset to locate the shared A subobject.

When a D object is sliced to a B object, the B object retains the pointer to the vbtable, ensuring that the A subobject can still be found at the correct offset.

8. Summary and Best Practices
Inheritance in C++ introduces complex syntax, especially multiple and diamond inheritance. Such complexity can lead to performance issues and code that is hard to analyze. It is generally advised to avoid diamond inheritance. Many modern OO languages like Java do not support multiple inheritance, considering it a potential source of problems.
A crucial design decision is choosing between inheritance and composition:
- Inheritance represents an is-a relationship. Every derived class object is a base class object.
- Composition represents a has-a relationship. A class contains an object of another class.
Example of inheritance (is-a):
class Phone {
protected:
std::string colour_;
std::string model_;
};
class MiPhone : public Phone {
public:
void use() {
std::cout << "This is a MiPhone" << std::endl;
}
};
Example of composition (has-a):
class Camera {
protected:
std::string brand_;
size_t size_;
};
class Phone {
protected:
std::string colour_;
std::string model_;
Camera camera_; // Phone has a Camera
};
When a relationship can be modeled both ways, prefer composition over inheritance for the following reasons:
- White-box reuse vs. Black-box reuse: Inheritance exposes base class internals to derived classes (white-box), breaking encapsulation and creating tight coupling. Composition uses well-defined interfaces (black-box), hiding internal details and reducing coupling.
- Encapsulation and maintainability: Composition results in loose coupling, making the code easier to maintain.
- Polymorphism: Inheritance is necessary to achieve polymorphism (virtual functions). However, if both approaches are viable, composition is generally preferred for its flexibility and lower dependency.