Inheritance in C++

What is Inheritance?

Inheritance is a core concept in object-oriented programming that allows a class (called the derived class or child class) to inherit properties and behaviors from another class (called the base class or parent class). It establishes an “is-a” relationship between classes, enabling code reuse and hierarchical classification.

Purpose of Inheritance

Inheritance serves several key purposes in object-oriented programming:

  1. Code Reusability: Avoid rewriting the same code by inheriting common functionality
  2. Extensibility: Extend existing classes without modifying their source code
  3. Hierarchical Organization: Create meaningful class hierarchies that model real-world relationships
  4. Polymorphism: Enable objects of different classes to be treated as objects of a common base class
  5. Specialization: Allow derived classes to specialize behavior of base classes

Basic Syntax for Inheritance

class BaseClass {
    // Base class members
};

class DerivedClass : [access-specifier] BaseClass {
    // Derived class members
};

The access-specifier can be:

  • public: Base class’s public members remain public, protected remain protected
  • protected: Base class’s public and protected members become protected
  • private: Base class’s public and protected members become private

Simple Example of Inheritance

#include <iostream>
using namespace std;

// Base class
class Person {
protected:
    string name;
    int age;
    
public:
    // Constructor
    Person(string n, int a) : name(n), age(a) {}
    
    void introduce() {
        cout << "Hi, my name is " << name << " and I am " << age << " years old." << endl;
    }
};

// Derived class
class Student : public Person {
private:
    int studentId;
    string course;
    
public:
    // Constructor
    Student(string n, int a, int id, string c) 
        : Person(n, a), studentId(id), course(c) {}
    
    void study() {
        cout << name << " is studying " << course << "." << endl;
    }
    
    void displayDetails() {
        cout << "Student ID: " << studentId << endl;
        cout << "Name: " << name << endl;
        cout << "Age: " << age << endl;
        cout << "Course: " << course << endl;
    }
};

int main() {
    // Create a Person object
    Person person("John", 30);
    person.introduce();
    
    // Create a Student object
    Student student("Alice", 20, 12345, "Computer Science");
    student.introduce();  // Inherited from Person
    student.study();      // Defined in Student
    student.displayDetails();
    
    return 0;
}

Output:

Hi, my name is John and I am 30 years old.
Hi, my name is Alice and I am 20 years old.
Alice is studying Computer Science.
Student ID: 12345
Name: Alice
Age: 20
Course: Computer Science

Access Specifiers and Inheritance

The way members of the base class are inherited depends on two factors:

  1. The access specifier in the base class (public, protected, private)
  2. The mode of inheritance (public, protected, private)

Inheritance Access Rules

Base Class Memberpublic Inheritanceprotected Inheritanceprivate Inheritance
publicpublicprotectedprivate
protectedprotectedprotectedprivate
privateNot accessibleNot accessibleNot accessible

Example with Different Inheritance Modes

#include <iostream>
using namespace std;

class Base {
public:
    int publicVar;
protected:
    int protectedVar;
private:
    int privateVar;
    
public:
    Base() : publicVar(1), protectedVar(2), privateVar(3) {}
    
    void display() {
        cout << "Base class: public = " << publicVar 
             << ", protected = " << protectedVar 
             << ", private = " << privateVar << endl;
    }
};

// Public inheritance
class PublicDerived : public Base {
public:
    void access() {
        cout << "PublicDerived: can access public = " << publicVar;
        cout << ", protected = " << protectedVar << endl;
        // privateVar is not accessible here
    }
};

// Protected inheritance
class ProtectedDerived : protected Base {
public:
    void access() {
        cout << "ProtectedDerived: can access public = " << publicVar;
        cout << ", protected = " << protectedVar << endl;
        // privateVar is not accessible here
    }
};

// Private inheritance
class PrivateDerived : private Base {
public:
    void access() {
        cout << "PrivateDerived: can access public = " << publicVar;
        cout << ", protected = " << protectedVar << endl;
        // privateVar is not accessible here
    }
};

int main() {
    Base base;
    PublicDerived pubDerived;
    ProtectedDerived protDerived;
    PrivateDerived privDerived;
    
    // Access with Base object
    base.publicVar = 10;  // OK, public member
    // base.protectedVar = 20;  // Error, protected member
    // base.privateVar = 30;    // Error, private member
    
    // Access with PublicDerived object
    pubDerived.publicVar = 40;  // OK, public inheritance keeps public members public
    // pubDerived.protectedVar = 50;  // Error, protected member
    
    // Access with ProtectedDerived object
    // protDerived.publicVar = 60;  // Error, protected inheritance makes public members protected
    
    // Access with PrivateDerived object
    // privDerived.publicVar = 70;  // Error, private inheritance makes public members private
    
    // Call member functions
    base.display();
    pubDerived.display();  // Inherited as public
    // protDerived.display();  // Error, inherited as protected
    // privDerived.display();  // Error, inherited as private
    
    // Call derived class member functions
    pubDerived.access();
    protDerived.access();
    privDerived.access();
    
    return 0;
}

Types of Inheritance

C++ supports five types of inheritance, allowing for various class relationships:

1. Single Inheritance

A derived class inherits from only one base class.

class Base { /* ... */ };
class Derived : public Base { /* ... */ };

2. Multiple Inheritance

A derived class inherits from two or more base classes.

class Base1 { /* ... */ };
class Base2 { /* ... */ };
class Derived : public Base1, public Base2 { /* ... */ };

3. Multilevel Inheritance

A derived class inherits from a base class, which itself is derived from another base class.

class GrandParent { /* ... */ };
class Parent : public GrandParent { /* ... */ };
class Child : public Parent { /* ... */ };

4. Hierarchical Inheritance

Multiple derived classes inherit from a single base class.

class Base { /* ... */ };
class Derived1 : public Base { /* ... */ };
class Derived2 : public Base { /* ... */ };

5. Hybrid Inheritance

A combination of two or more types of inheritance.

class Base { /* ... */ };
class Derived1 : public Base { /* ... */ };
class Derived2 : public Base { /* ... */ };
class DerivedFromBoth : public Derived1, public Derived2 { /* ... */ };

Constructor and Destructor Execution in Inheritance

When an object of a derived class is created:

  1. Base class constructor is called first
  2. Derived class constructor is called next

When an object of a derived class is destroyed:

  1. Derived class destructor is called first
  2. Base class destructor is called next
#include <iostream>
using namespace std;

class Base {
public:
    Base() {
        cout << "Base constructor called" << endl;
    }
    
    ~Base() {
        cout << "Base destructor called" << endl;
    }
};

class Derived : public Base {
public:
    Derived() {
        cout << "Derived constructor called" << endl;
    }
    
    ~Derived() {
        cout << "Derived destructor called" << endl;
    }
};

int main() {
    cout << "Creating Derived object:" << endl;
    Derived d;
    cout << "Program ending..." << endl;
    
    return 0;
}

Output:

Creating Derived object:
Base constructor called
Derived constructor called
Program ending...
Derived destructor called
Base destructor called

Initializing Base Class with Parameters

To initialize a base class with specific values, you use the constructor initializer list:

#include <iostream>
using namespace std;

class Base {
private:
    int value;
    
public:
    Base(int v) : value(v) {
        cout << "Base constructor called with value " << value << endl;
    }
    
    int getValue() const {
        return value;
    }
};

class Derived : public Base {
private:
    int derivedValue;
    
public:
    // Initialize base class with parameter
    Derived(int b, int d) : Base(b), derivedValue(d) {
        cout << "Derived constructor called with value " << derivedValue << endl;
    }
    
    void display() {
        cout << "Base value: " << getValue() << endl;
        cout << "Derived value: " << derivedValue << endl;
    }
};

int main() {
    Derived d(10, 20);
    d.display();
    
    return 0;
}

Output:

Base constructor called with value 10
Derived constructor called with value 20
Base value: 10
Derived value: 20

Method Overriding

Method overriding occurs when a derived class provides a specific implementation for a method that is already defined in its base class. This allows the derived class to customize or completely replace the behavior of the base class method.

#include <iostream>
using namespace std;

class Animal {
public:
    void makeSound() {
        cout << "Animal makes a sound" << endl;
    }
    
    void eat() {
        cout << "Animal eats food" << endl;
    }
};

class Dog : public Animal {
public:
    // Override the makeSound method
    void makeSound() {
        cout << "Dog barks: Woof! Woof!" << endl;
    }
    
    // Not overriding eat method
};

class Cat : public Animal {
public:
    // Override the makeSound method
    void makeSound() {
        cout << "Cat meows: Meow!" << endl;
    }
    
    // Override the eat method
    void eat() {
        cout << "Cat eats fish" << endl;
    }
};

int main() {
    Animal animal;
    Dog dog;
    Cat cat;
    
    cout << "Animal sounds:" << endl;
    animal.makeSound();
    dog.makeSound();
    cat.makeSound();
    
    cout << "\nEating habits:" << endl;
    animal.eat();
    dog.eat();     // Uses Animal::eat()
    cat.eat();
    
    return 0;
}

Output:

Animal sounds:
Animal makes a sound
Dog barks: Woof! Woof!
Cat meows: Meow!

Eating habits:
Animal eats food
Animal eats food
Cat eats fish

Using the override Keyword (C++11)

In modern C++, it’s good practice to use the override keyword to explicitly indicate when you’re overriding a base class method. This helps catch errors at compile time.

class Base {
public:
    virtual void foo() {
        cout << "Base::foo()" << endl;
    }
};

class Derived : public Base {
public:
    void foo() override {  // Compiler checks if this really overrides a base class method
        cout << "Derived::foo()" << endl;
    }
    
    // This would cause a compile error because there's no base class method with this signature
    // void fooo() override { }
};

Protected Members

Protected members are accessible within the class they are defined in, as well as in classes derived from that class, but not outside the class hierarchy.

#include <iostream>
using namespace std;

class Base {
private:
    int privateVar;
    
protected:
    int protectedVar;
    
public:
    int publicVar;
    
    Base() : privateVar(1), protectedVar(2), publicVar(3) {}
    
    void display() {
        cout << "Base: privateVar = " << privateVar
             << ", protectedVar = " << protectedVar
             << ", publicVar = " << publicVar << endl;
    }
};

class Derived : public Base {
public:
    void accessAndModify() {
        // privateVar = 10;  // Error, can't access private members
        protectedVar = 20;   // OK, can access protected members
        publicVar = 30;      // OK, can access public members
        
        cout << "Derived: protectedVar = " << protectedVar
             << ", publicVar = " << publicVar << endl;
    }
};

int main() {
    Base b;
    Derived d;
    
    b.display();
    
    // Can't access protected members from outside the class hierarchy
    // cout << b.protectedVar;  // Error
    cout << b.publicVar << endl;  // OK
    
    d.accessAndModify();
    d.display();  // Inherited from Base
    
    return 0;
}

Output:

Base: privateVar = 1, protectedVar = 2, publicVar = 3
3
Derived: protectedVar = 20, publicVar = 30
Base: privateVar = 1, protectedVar = 20, publicVar = 30

Accessing Base Class Methods

Sometimes, a derived class needs to access the base class version of an overridden method. This can be done using the scope resolution operator (::).

#include <iostream>
using namespace std;

class Base {
public:
    void display() {
        cout << "Display method of Base class" << endl;
    }
};

class Derived : public Base {
public:
    // Override display method
    void display() {
        cout << "Display method of Derived class" << endl;
    }
    
    // Method that calls both versions
    void showBoth() {
        // Call derived class version
        display();
        
        // Call base class version
        Base::display();
    }
};

int main() {
    Derived d;
    
    cout << "Calling display directly:" << endl;
    d.display();  // Calls Derived::display()
    
    cout << "\nCalling both versions:" << endl;
    d.showBoth();
    
    cout << "\nExplicitly calling base version:" << endl;
    d.Base::display();  // Explicitly call Base::display()
    
    return 0;
}

Output:

Calling display directly:
Display method of Derived class

Calling both versions:
Display method of Derived class
Display method of Base class

Explicitly calling base version:
Display method of Base class

Common Pitfalls in Inheritance

  1. Slicing Problem: When a derived class object is assigned to a base class object, the derived class-specific members are “sliced off”.
Derived d;
Base b = d;  // Slicing - only the Base part of d is copied to b
  1. Multiple Inheritance Diamond Problem: When a class inherits from two classes that both inherit from a common base class, ambiguity can arise.
class A { /* ... */ };
class B : public A { /* ... */ };
class C : public A { /* ... */ };
class D : public B, public C { /* ... */ };  // D has two copies of A's members
  1. Incorrect Access Specifiers: Using the wrong access specifier can lead to unexpected behavior or restricted access.

  2. Not Using Virtual Destructors: If a base class doesn’t have a virtual destructor, deleting a derived class object through a base class pointer can lead to undefined behavior.

Best Practices for Inheritance

  1. Use public inheritance for “is-a” relationships (e.g., a Student is a Person).

  2. Consider composition over inheritance for “has-a” relationships (e.g., a Car has an Engine).

  3. Make destructors virtual in base classes that might be inherited from.

  4. Use the override keyword (C++11) to explicitly indicate method overriding.

  5. Avoid deep inheritance hierarchies as they can become hard to understand and maintain.

  6. Think carefully about access specifiers to ensure appropriate encapsulation.

  7. Use protected members for data that derived classes need to access but should be hidden from external code.

  8. Initialize base classes properly using constructor initializer lists.

Summary

Inheritance is a powerful feature of object-oriented programming that allows classes to inherit attributes and behaviors from other classes. It promotes code reuse, establishes hierarchical relationships, and enables polymorphism. By understanding the different types of inheritance, access control, and best practices, you can effectively use inheritance to create well-structured, maintainable code in C++.