Inheritance in C++: Building Class Hierarchies

Learn C++ inheritance to build class hierarchies. Master base classes, derived classes, code reuse, and create extensible object-oriented programs.

Inheritance in C++: Building Class Hierarchies

Inheritance in C++ is an object-oriented programming mechanism that allows a new class (derived class) to acquire properties and behaviors from an existing class (base class), enabling code reuse, establishing hierarchical relationships, and creating extensible software architectures. This fundamental concept promotes the “is-a” relationship between classes and reduces code duplication through shared functionality.

Introduction: The Foundation of Object-Oriented Design

Inheritance stands as one of the most powerful features in C++ and object-oriented programming as a whole. It enables you to build upon existing code, creating new classes that extend and specialize the functionality of existing ones without rewriting everything from scratch. This isn’t just about convenience—it’s about creating logical, maintainable software architectures that mirror real-world relationships.

Consider the natural world: a dog is an animal, a car is a vehicle, a savings account is a bank account. These “is-a” relationships form the conceptual foundation of inheritance. In C++, you can model these relationships directly in code, creating base classes that define common characteristics and derived classes that add specific features.

When you master inheritance, you unlock the ability to create sophisticated software systems where changes in common functionality automatically propagate to all related classes, where new features can be added without disrupting existing code, and where your codebase becomes a well-organized hierarchy of related concepts rather than a collection of disconnected pieces.

This comprehensive guide will take you from the basics of single inheritance through advanced topics like multiple inheritance, virtual functions, and abstract classes. You’ll learn not just the syntax, but the design principles that make inheritance a cornerstone of professional C++ development.

What is Inheritance? Understanding the Core Concept

Inheritance is the mechanism by which one class (the derived class or child class) inherits properties and methods from another class (the base class or parent class). The derived class automatically receives all the members of the base class and can add its own unique members or modify inherited behavior.

Think of inheritance like a family tree or an organizational chart. Just as a manager inherits general employee attributes but has additional responsibilities, a derived class inherits base class features while adding specialized capabilities.

The Benefits of Inheritance

Code Reusability: Write common functionality once in a base class, then reuse it in multiple derived classes. If you have ten types of vehicles, you don’t write the “start engine” code ten times—you write it once in the Vehicle base class.

Logical Organization: Inheritance creates intuitive hierarchies that mirror real-world relationships. Your code structure becomes self-documenting, making it easier for others (and future you) to understand.

Extensibility: Add new functionality by creating new derived classes without modifying existing code. This adheres to the Open/Closed Principle: software should be open for extension but closed for modification.

Polymorphism: Inheritance enables polymorphism, allowing you to write code that works with base class pointers but executes derived class behavior—a powerful technique we’ll explore in depth.

Maintenance: Fix a bug or improve a method in the base class, and all derived classes automatically benefit from the improvement.

Basic Inheritance Syntax: Creating Your First Hierarchy

The fundamental syntax for inheritance in C++ uses a colon followed by an access specifier and the base class name:

C++
#include <iostream>
#include <string>
using namespace std;

// Base class - defines common properties
class Animal {
protected:
    string name;
    int age;
    
public:
    Animal(string n, int a) : name(n), age(a) {
        cout << "Animal constructor called" << endl;
    }
    
    void eat() {
        cout << name << " is eating." << endl;
    }
    
    void sleep() {
        cout << name << " is sleeping." << endl;
    }
    
    void displayInfo() {
        cout << "Name: " << name << endl;
        cout << "Age: " << age << " years" << endl;
    }
};

// Derived class - inherits from Animal and adds specific features
class Dog : public Animal {
private:
    string breed;
    
public:
    Dog(string n, int a, string b) : Animal(n, a), breed(b) {
        cout << "Dog constructor called" << endl;
    }
    
    void bark() {
        cout << name << " says: Woof! Woof!" << endl;
    }
    
    void displayBreed() {
        cout << "Breed: " << breed << endl;
    }
};

int main() {
    Dog myDog("Buddy", 3, "Golden Retriever");
    
    // Can use inherited methods
    myDog.eat();
    myDog.sleep();
    myDog.displayInfo();
    
    // Can use Dog-specific methods
    myDog.bark();
    myDog.displayBreed();
    
    return 0;
}

Output:

C++
Animal constructor called
Dog constructor called
Buddy is eating.
Buddy is sleeping.
Name: Buddy
Age: 3 years
Woof! Woof!
Breed: Golden Retriever

Notice how the Dog class automatically has access to eat(), sleep(), and displayInfo() methods without defining them. The Dog object is both a Dog and an Animal—it inherits all Animal capabilities while adding its own.

Types of Inheritance Access Specifiers

When you inherit from a base class, you specify the access level using public, protected, or private inheritance. This determines how the inherited members are accessible in the derived class.

Public Inheritance (Most Common)

Public inheritance maintains the access levels of base class members in the derived class. This is the most common and intuitive form of inheritance, representing a true “is-a” relationship.

C++
class Vehicle {
protected:
    string brand;
    int year;
    
public:
    Vehicle(string b, int y) : brand(b), year(y) {}
    
    void displayBrand() {
        cout << "Brand: " << brand << endl;
    }
};

// Public inheritance - Car IS-A Vehicle
class Car : public Vehicle {
private:
    int numDoors;
    
public:
    Car(string b, int y, int doors) : Vehicle(b, y), numDoors(doors) {}
    
    void displayCarInfo() {
        displayBrand();  // public method remains public
        cout << "Year: " << year << endl;  // protected member accessible
        cout << "Doors: " << numDoors << endl;
    }
};

int main() {
    Car myCar("Toyota", 2024, 4);
    myCar.displayBrand();  // Can call public inherited method
    myCar.displayCarInfo();
}

Protected Inheritance

Protected inheritance makes all public and protected members of the base class protected in the derived class. This is less common but useful when you want to hide the base class interface from external code while allowing further derived classes to access it.

C++
class Engine {
public:
    void start() {
        cout << "Engine starting..." << endl;
    }
    
    void stop() {
        cout << "Engine stopping..." << endl;
    }
};

// Protected inheritance
class Car : protected Engine {
public:
    void drive() {
        start();  // Can use Engine methods internally
        cout << "Car is driving" << endl;
    }
    
    void park() {
        cout << "Car is parking" << endl;
        stop();   // Can use Engine methods internally
    }
};

int main() {
    Car myCar;
    myCar.drive();
    myCar.park();
    
    // myCar.start();  // Error! start() is protected in Car
}

Private Inheritance

Private inheritance makes all base class members private in the derived class, effectively hiding them from both external code and further derived classes. This represents a “implemented-in-terms-of” relationship rather than “is-a.”

C++
class Timer {
public:
    void start() {
        cout << "Timer started" << endl;
    }
    
    void stop() {
        cout << "Timer stopped" << endl;
    }
};

// Private inheritance - implementation detail, not IS-A
class Benchmark : private Timer {
public:
    void runBenchmark() {
        start();  // Use Timer internally
        cout << "Running performance test..." << endl;
        stop();
    }
};

int main() {
    Benchmark test;
    test.runBenchmark();
    
    // test.start();  // Error! Timer methods are private in Benchmark
}

Inheritance Access Control Table

Base Class MemberPublic InheritanceProtected InheritancePrivate Inheritance
publicpublicprotectedprivate
protectedprotectedprotectedprivate
privatenot accessiblenot accessiblenot accessible

Constructors and Destructors in Inheritance

Understanding how constructors and destructors work in inheritance hierarchies is crucial for proper resource management and object initialization.

Constructor Calling Order

When you create a derived class object, constructors are called in a specific order: base class constructor first, then derived class constructor. This makes sense—you need to build the foundation before adding specialized features.

C++
class Base {
public:
    Base() {
        cout << "1. Base constructor called" << endl;
    }
    
    Base(int x) {
        cout << "1. Base constructor with parameter: " << x << endl;
    }
};

class Derived : public Base {
public:
    Derived() {
        cout << "2. Derived constructor called" << endl;
    }
    
    Derived(int x) : Base(x) {  // Explicitly call base constructor
        cout << "2. Derived constructor called" << endl;
    }
};

int main() {
    cout << "Creating object without parameters:" << endl;
    Derived obj1;
    
    cout << "\nCreating object with parameters:" << endl;
    Derived obj2(42);
}

Output:

C++
Creating object without parameters:
1. Base constructor called
2. Derived constructor called

Creating object with parameters:
1. Base constructor with parameter: 42
2. Derived constructor called

Destructor Calling Order

Destructors are called in reverse order: derived class destructor first, then base class destructor. This ensures proper cleanup—specialized resources are released before general ones.

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

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

int main() {
    cout << "Creating object:" << endl;
    Derived obj;
    cout << "\nObject going out of scope:" << endl;
    // Destructors called automatically
}

Output:

C++
Creating object:
Base constructor
Derived constructor

Object going out of scope:
Derived destructor
Base destructor

Passing Parameters to Base Class Constructors

You must explicitly pass parameters to base class constructors using the member initializer list:

C++
class Person {
protected:
    string name;
    int age;
    
public:
    Person(string n, int a) : name(n), age(a) {
        cout << "Person created: " << name << endl;
    }
};

class Student : public Person {
private:
    string studentId;
    double gpa;
    
public:
    // Must call Person constructor with required parameters
    Student(string n, int a, string id, double g) 
        : Person(n, a), studentId(id), gpa(g) {
        cout << "Student enrolled: " << studentId << endl;
    }
    
    void displayInfo() {
        cout << "Name: " << name << endl;
        cout << "Age: " << age << endl;
        cout << "ID: " << studentId << endl;
        cout << "GPA: " << gpa << endl;
    }
};

int main() {
    Student student("Alice Johnson", 20, "S12345", 3.8);
    student.displayInfo();
}

Method Overriding: Customizing Inherited Behavior

Derived classes can override base class methods to provide specialized implementations. This is fundamental to polymorphism and allows derived classes to customize inherited behavior.

C++
class Shape {
protected:
    string color;
    
public:
    Shape(string c) : color(c) {}
    
    // Virtual function can be overridden
    virtual void draw() {
        cout << "Drawing a generic shape in " << color << endl;
    }
    
    virtual double getArea() {
        return 0.0;
    }
    
    void displayColor() {
        cout << "Color: " << color << endl;
    }
};

class Circle : public Shape {
private:
    double radius;
    
public:
    Circle(string c, double r) : Shape(c), radius(r) {}
    
    // Override the draw method
    void draw() override {
        cout << "Drawing a circle with radius " << radius 
             << " in " << color << endl;
    }
    
    // Override the getArea method
    double getArea() override {
        return 3.14159 * radius * radius;
    }
};

class Rectangle : public Shape {
private:
    double width;
    double height;
    
public:
    Rectangle(string c, double w, double h) 
        : Shape(c), width(w), height(h) {}
    
    void draw() override {
        cout << "Drawing a rectangle " << width << "x" << height 
             << " in " << color << endl;
    }
    
    double getArea() override {
        return width * height;
    }
};

int main() {
    Circle circle("red", 5.0);
    Rectangle rect("blue", 4.0, 6.0);
    
    circle.draw();
    cout << "Circle area: " << circle.getArea() << endl;
    
    rect.draw();
    cout << "Rectangle area: " << rect.getArea() << endl;
}

Output:

C++
Drawing a circle with radius 5 in red
Circle area: 78.5397
Drawing a rectangle 4x6 in blue
Rectangle area: 24

The override keyword (C++11) is optional but highly recommended—it tells the compiler you intend to override a base class method, catching errors if the method signature doesn’t match.

Multi-Level Inheritance: Building Deeper Hierarchies

Multi-level inheritance creates chains of inheritance where a class inherits from a derived class, creating a grandfather-father-son relationship.

C++
class LivingBeing {
protected:
    bool isAlive;
    
public:
    LivingBeing() : isAlive(true) {
        cout << "LivingBeing constructor" << endl;
    }
    
    void breathe() {
        cout << "Breathing..." << endl;
    }
};

class Animal : public LivingBeing {
protected:
    int legs;
    
public:
    Animal(int l) : legs(l) {
        cout << "Animal constructor" << endl;
    }
    
    void move() {
        cout << "Moving with " << legs << " legs" << endl;
    }
};

class Dog : public Animal {
private:
    string breed;
    
public:
    Dog(string b) : Animal(4), breed(b) {
        cout << "Dog constructor" << endl;
    }
    
    void bark() {
        cout << breed << " is barking!" << endl;
    }
    
    void displayAll() {
        breathe();  // From LivingBeing
        move();     // From Animal
        bark();     // From Dog
    }
};

int main() {
    Dog myDog("Labrador");
    cout << "\nDog capabilities:" << endl;
    myDog.displayAll();
}

Output:

C++
LivingBeing constructor
Animal constructor
Dog constructor

Dog capabilities:
Breathing...
Moving with 4 legs
Labrador is barking!

Multiple Inheritance: Inheriting from Multiple Base Classes

C++ supports multiple inheritance, where a class can inherit from more than one base class. This is powerful but must be used carefully to avoid complexity and the diamond problem.

C++
class Flyer {
protected:
    double maxAltitude;
    
public:
    Flyer(double altitude) : maxAltitude(altitude) {}
    
    void fly() {
        cout << "Flying at max altitude: " << maxAltitude << " feet" << endl;
    }
};

class Swimmer {
protected:
    double maxDepth;
    
public:
    Swimmer(double depth) : maxDepth(depth) {}
    
    void swim() {
        cout << "Swimming at max depth: " << maxDepth << " feet" << endl;
    }
};

// Multiple inheritance - Duck can both fly and swim
class Duck : public Flyer, public Swimmer {
private:
    string name;
    
public:
    Duck(string n, double altitude, double depth) 
        : Flyer(altitude), Swimmer(depth), name(n) {
        cout << "Duck " << name << " created" << endl;
    }
    
    void quack() {
        cout << name << " says: Quack!" << endl;
    }
    
    void showCapabilities() {
        cout << "\n" << name << "'s capabilities:" << endl;
        fly();
        swim();
        quack();
    }
};

int main() {
    Duck mallard("Donald", 5000.0, 10.0);
    mallard.showCapabilities();
}

Output:

C++
Duck Donald created

Donald's capabilities:
Flying at max altitude: 5000 feet
Swimming at max depth: 10 feet
Donald says: Quack!

The Diamond Problem and Virtual Inheritance

The diamond problem occurs when a class inherits from two classes that share a common base class, creating ambiguity. Virtual inheritance solves this:

C++
class Device {
protected:
    string deviceId;
    
public:
    Device(string id) : deviceId(id) {
        cout << "Device constructor: " << deviceId << endl;
    }
    
    void powerOn() {
        cout << "Device " << deviceId << " powered on" << endl;
    }
};

// Virtual inheritance prevents duplicate Device instances
class Scanner : virtual public Device {
public:
    Scanner(string id) : Device(id) {
        cout << "Scanner constructor" << endl;
    }
    
    void scan() {
        cout << "Scanning document..." << endl;
    }
};

class Printer : virtual public Device {
public:
    Printer(string id) : Device(id) {
        cout << "Printer constructor" << endl;
    }
    
    void print() {
        cout << "Printing document..." << endl;
    }
};

class MultiFunctionDevice : public Scanner, public Printer {
public:
    MultiFunctionDevice(string id) 
        : Device(id), Scanner(id), Printer(id) {
        cout << "MultiFunctionDevice constructor" << endl;
    }
    
    void copyDocument() {
        scan();
        print();
    }
};

int main() {
    MultiFunctionDevice mfd("MFD-001");
    cout << "\nUsing device:" << endl;
    mfd.powerOn();  // No ambiguity - only one Device base
    mfd.copyDocument();
}

Polymorphism Through Inheritance

Polymorphism allows you to treat derived class objects as base class objects, enabling flexible and extensible code.

C++
class Employee {
protected:
    string name;
    int id;
    double baseSalary;
    
public:
    Employee(string n, int i, double salary) 
        : name(n), id(i), baseSalary(salary) {}
    
    // Virtual function for polymorphic behavior
    virtual double calculatePay() {
        return baseSalary;
    }
    
    virtual void displayInfo() {
        cout << "Employee: " << name << " (ID: " << id << ")" << endl;
        cout << "Pay: $" << calculatePay() << endl;
    }
    
    virtual ~Employee() {}  // Virtual destructor important for polymorphism
};

class Manager : public Employee {
private:
    double bonus;
    
public:
    Manager(string n, int i, double salary, double b) 
        : Employee(n, i, salary), bonus(b) {}
    
    double calculatePay() override {
        return baseSalary + bonus;
    }
    
    void displayInfo() override {
        cout << "Manager: " << name << " (ID: " << id << ")" << endl;
        cout << "Base Salary: $" << baseSalary << endl;
        cout << "Bonus: $" << bonus << endl;
        cout << "Total Pay: $" << calculatePay() << endl;
    }
};

class Developer : public Employee {
private:
    int projectsCompleted;
    double projectBonus;
    
public:
    Developer(string n, int i, double salary, int projects) 
        : Employee(n, i, salary), projectsCompleted(projects), 
          projectBonus(500.0) {}
    
    double calculatePay() override {
        return baseSalary + (projectsCompleted * projectBonus);
    }
    
    void displayInfo() override {
        cout << "Developer: " << name << " (ID: " << id << ")" << endl;
        cout << "Projects Completed: " << projectsCompleted << endl;
        cout << "Total Pay: $" << calculatePay() << endl;
    }
};

void processPayroll(Employee* employees[], int count) {
    double totalPayroll = 0;
    
    cout << "=== Payroll Processing ===" << endl;
    for (int i = 0; i < count; i++) {
        employees[i]->displayInfo();
        totalPayroll += employees[i]->calculatePay();
        cout << "---" << endl;
    }
    
    cout << "Total Payroll: $" << totalPayroll << endl;
}

int main() {
    Employee* employees[3];
    
    employees[0] = new Employee("John Smith", 101, 50000);
    employees[1] = new Manager("Sarah Johnson", 102, 70000, 15000);
    employees[2] = new Developer("Mike Chen", 103, 65000, 8);
    
    processPayroll(employees, 3);
    
    // Clean up
    for (int i = 0; i < 3; i++) {
        delete employees[i];
    }
    
    return 0;
}

This demonstrates polymorphism’s power: the processPayroll function works with Employee pointers but correctly calls the appropriate calculatePay() and displayInfo() methods for each actual object type.

Abstract Classes and Pure Virtual Functions

Abstract classes define interfaces that derived classes must implement. They cannot be instantiated and typically contain pure virtual functions.

C++
class Shape {
protected:
    string color;
    
public:
    Shape(string c) : color(c) {}
    
    // Pure virtual functions - must be overridden
    virtual double getArea() = 0;
    virtual double getPerimeter() = 0;
    virtual void draw() = 0;
    
    // Regular virtual function - can be overridden
    virtual void displayColor() {
        cout << "Color: " << color << endl;
    }
    
    virtual ~Shape() {}
};

class Circle : public Shape {
private:
    double radius;
    
public:
    Circle(string c, double r) : Shape(c), radius(r) {}
    
    double getArea() override {
        return 3.14159 * radius * radius;
    }
    
    double getPerimeter() override {
        return 2 * 3.14159 * radius;
    }
    
    void draw() override {
        cout << "Drawing a " << color << " circle" << endl;
    }
};

class Triangle : public Shape {
private:
    double side1, side2, side3;
    
public:
    Triangle(string c, double s1, double s2, double s3) 
        : Shape(c), side1(s1), side2(s2), side3(s3) {}
    
    double getArea() override {
        // Heron's formula
        double s = (side1 + side2 + side3) / 2;
        return sqrt(s * (s - side1) * (s - side2) * (s - side3));
    }
    
    double getPerimeter() override {
        return side1 + side2 + side3;
    }
    
    void draw() override {
        cout << "Drawing a " << color << " triangle" << endl;
    }
};

void analyzeShape(Shape* shape) {
    shape->draw();
    shape->displayColor();
    cout << "Area: " << shape->getArea() << endl;
    cout << "Perimeter: " << shape->getPerimeter() << endl;
    cout << endl;
}

int main() {
    // Shape shape("red");  // Error! Cannot instantiate abstract class
    
    Shape* shapes[2];
    shapes[0] = new Circle("red", 5.0);
    shapes[1] = new Triangle("blue", 3.0, 4.0, 5.0);
    
    for (int i = 0; i < 2; i++) {
        analyzeShape(shapes[i]);
        delete shapes[i];
    }
    
    return 0;
}

Protected Members: The Inheritance Sweet Spot

Protected members strike a balance between encapsulation and inheritance flexibility. They’re accessible in derived classes but hidden from external code.

C++
class BankAccount {
protected:
    double balance;
    string accountNumber;
    
    // Protected helper method for derived classes
    void logTransaction(string transaction) {
        cout << "[" << accountNumber << "] " << transaction << endl;
    }
    
private:
    string pin;  // Truly private - not even derived classes can access
    
public:
    BankAccount(string accNum, double initialBalance, string p) 
        : accountNumber(accNum), balance(initialBalance), pin(p) {}
    
    virtual void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
            logTransaction("Deposited $" + to_string(amount));
        }
    }
    
    double getBalance() const {
        return balance;
    }
};

class SavingsAccount : public BankAccount {
private:
    double interestRate;
    
public:
    SavingsAccount(string accNum, double initialBalance, string p, double rate) 
        : BankAccount(accNum, initialBalance, p), interestRate(rate) {}
    
    void applyInterest() {
        // Can access protected balance and logTransaction
        double interest = balance * interestRate;
        balance += interest;
        logTransaction("Interest applied: $" + to_string(interest));
    }
    
    void deposit(double amount) override {
        if (amount > 0) {
            balance += amount;  // Can access protected balance
            
            // Apply bonus for large deposits
            if (amount >= 1000) {
                double bonus = amount * 0.01;
                balance += bonus;
                logTransaction("Deposit bonus: $" + to_string(bonus));
            }
            
            logTransaction("Deposited $" + to_string(amount));
        }
    }
};

int main() {
    SavingsAccount savings("SAV-001", 1000.0, "1234", 0.05);
    
    cout << "Initial balance: $" << savings.getBalance() << endl;
    
    savings.deposit(1500);
    savings.applyInterest();
    
    cout << "Final balance: $" << savings.getBalance() << endl;
}

Real-World Example: Building a Complete Hierarchy

Let’s create a comprehensive employee management system demonstrating multiple inheritance concepts:

C++
#include <iostream>
#include <string>
#include <vector>
using namespace std;

// Abstract base class
class Person {
protected:
    string name;
    int age;
    string address;
    
public:
    Person(string n, int a, string addr) 
        : name(n), age(a), address(addr) {}
    
    virtual void displayInfo() = 0;  // Pure virtual
    
    string getName() const { return name; }
    
    virtual ~Person() {}
};

// Employee base class
class Employee : public Person {
protected:
    int employeeId;
    string department;
    double baseSalary;
    
public:
    Employee(string n, int a, string addr, int id, string dept, double salary) 
        : Person(n, a, addr), employeeId(id), department(dept), 
          baseSalary(salary) {}
    
    virtual double calculateSalary() {
        return baseSalary;
    }
    
    void displayInfo() override {
        cout << "Name: " << name << endl;
        cout << "Employee ID: " << employeeId << endl;
        cout << "Department: " << department << endl;
        cout << "Salary: $" << calculateSalary() << endl;
    }
    
    int getEmployeeId() const { return employeeId; }
};

// Manager class - has leadership responsibilities
class Manager : public Employee {
private:
    vector<int> teamMemberIds;
    double managementBonus;
    
public:
    Manager(string n, int a, string addr, int id, string dept, 
            double salary, double bonus) 
        : Employee(n, a, addr, id, dept, salary), 
          managementBonus(bonus) {}
    
    void addTeamMember(int memberId) {
        teamMemberIds.push_back(memberId);
    }
    
    double calculateSalary() override {
        return baseSalary + managementBonus + (teamMemberIds.size() * 500);
    }
    
    void displayInfo() override {
        cout << "=== MANAGER ===" << endl;
        Employee::displayInfo();  // Call base class version
        cout << "Team Size: " << teamMemberIds.size() << endl;
        cout << "Management Bonus: $" << managementBonus << endl;
    }
};

// Developer class - technical employee
class Developer : public Employee {
private:
    string programmingLanguage;
    int projectsCompleted;
    
public:
    Developer(string n, int a, string addr, int id, string dept, 
              double salary, string lang) 
        : Employee(n, a, addr, id, dept, salary), 
          programmingLanguage(lang), projectsCompleted(0) {}
    
    void completeProject() {
        projectsCompleted++;
        cout << name << " completed a project in " 
             << programmingLanguage << endl;
    }
    
    double calculateSalary() override {
        return baseSalary + (projectsCompleted * 1000);
    }
    
    void displayInfo() override {
        cout << "=== DEVELOPER ===" << endl;
        Employee::displayInfo();
        cout << "Programming Language: " << programmingLanguage << endl;
        cout << "Projects Completed: " << projectsCompleted << endl;
    }
};

// Intern class - temporary employee
class Intern : public Employee {
private:
    string university;
    int monthsRemaining;
    
public:
    Intern(string n, int a, string addr, int id, string dept, 
           double salary, string uni, int months) 
        : Employee(n, a, addr, id, dept, salary), 
          university(uni), monthsRemaining(months) {}
    
    void displayInfo() override {
        cout << "=== INTERN ===" << endl;
        Employee::displayInfo();
        cout << "University: " << university << endl;
        cout << "Months Remaining: " << monthsRemaining << endl;
    }
    
    void progressMonth() {
        if (monthsRemaining > 0) {
            monthsRemaining--;
        }
    }
};

// Company class to manage employees
class Company {
private:
    string companyName;
    vector<Employee*> employees;
    
public:
    Company(string name) : companyName(name) {}
    
    void hireEmployee(Employee* emp) {
        employees.push_back(emp);
        cout << emp->getName() << " hired!" << endl;
    }
    
    void displayAllEmployees() {
        cout << "\n========== " << companyName << " Employees ==========" << endl;
        for (Employee* emp : employees) {
            emp->displayInfo();
            cout << "---" << endl;
        }
    }
    
    double calculateTotalPayroll() {
        double total = 0;
        for (Employee* emp : employees) {
            total += emp->calculateSalary();
        }
        return total;
    }
    
    ~Company() {
        for (Employee* emp : employees) {
            delete emp;
        }
    }
};

int main() {
    Company techCorp("TechCorp Solutions");
    
    // Hire different types of employees
    Manager* mgr = new Manager("Sarah Williams", 35, "123 Main St", 
                               1001, "Engineering", 90000, 15000);
    
    Developer* dev1 = new Developer("John Smith", 28, "456 Oak Ave", 
                                    1002, "Engineering", 75000, "C++");
    
    Developer* dev2 = new Developer("Emily Chen", 26, "789 Pine Rd", 
                                    1003, "Engineering", 72000, "Python");
    
    Intern* intern = new Intern("Mike Johnson", 22, "321 Elm St", 
                               1004, "Engineering", 35000, "MIT", 6);
    
    techCorp.hireEmployee(mgr);
    techCorp.hireEmployee(dev1);
    techCorp.hireEmployee(dev2);
    techCorp.hireEmployee(intern);
    
    cout << "\n--- Setting up team ---" << endl;
    mgr->addTeamMember(1002);
    mgr->addTeamMember(1003);
    mgr->addTeamMember(1004);
    
    cout << "\n--- Developers working ---" << endl;
    dev1->completeProject();
    dev1->completeProject();
    dev2->completeProject();
    
    // Display all employees
    techCorp.displayAllEmployees();
    
    // Calculate payroll
    cout << "\nTotal Company Payroll: $" 
         << techCorp.calculateTotalPayroll() << endl;
    
    return 0;
}

Common Inheritance Mistakes and Solutions

Mistake 1: Forgetting to Call Base Constructor

C++
// Bad - base class default constructor must exist
class Base {
public:
    Base(int x) {
        cout << "Base constructor: " << x << endl;
    }
};

class Derived : public Base {
public:
    Derived() {  // Error! No Base() constructor
        cout << "Derived constructor" << endl;
    }
};

// Good - explicitly call base constructor
class FixedDerived : public Base {
public:
    FixedDerived() : Base(0) {  // Provide required parameter
        cout << "Derived constructor" << endl;
    }
};

Mistake 2: Hiding Base Class Methods Unintentionally

C++
class Base {
public:
    void display() {
        cout << "Base display" << endl;
    }
    
    void display(int x) {
        cout << "Base display with int: " << x << endl;
    }
};

class Derived : public Base {
public:
    void display() {  // Hides ALL base class display methods
        cout << "Derived display" << endl;
    }
};

int main() {
    Derived d;
    d.display();     // OK - calls Derived::display()
    // d.display(5); // Error! Base::display(int) is hidden
    
    // Solution: use 'using' declaration
    class FixedDerived : public Base {
    public:
        using Base::display;  // Make base versions visible
        
        void display() {
            cout << "FixedDerived display" << endl;
        }
    };
    
    FixedDerived fd;
    fd.display();    // Calls FixedDerived::display()
    fd.display(5);   // Calls Base::display(int) - now accessible!
}

Mistake 3: Slicing Objects

C++
class Base {
public:
    int baseValue;
    Base() : baseValue(1) {}
    virtual void show() {
        cout << "Base: " << baseValue << endl;
    }
};

class Derived : public Base {
public:
    int derivedValue;
    Derived() : derivedValue(2) {}
    void show() override {
        cout << "Derived: " << baseValue << ", " << derivedValue << endl;
    }
};

void badFunction(Base obj) {  // Takes by value - slicing occurs!
    obj.show();
}

void goodFunction(Base& obj) {  // Takes by reference - no slicing
    obj.show();
}

int main() {
    Derived d;
    
    badFunction(d);   // Slicing! Copies only Base part
    goodFunction(d);  // No slicing - polymorphism works
}

Best Practices for Inheritance

1. Prefer Composition Over Inheritance When Appropriate

Inheritance represents “is-a” relationships. If the relationship is “has-a,” use composition instead:

C++
// Bad - Car is not an Engine
class Car : public Engine {
    // ...
};

// Good - Car has an Engine
class Car {
private:
    Engine engine;
    // ...
};

2. Make Base Class Destructors Virtual

When using polymorphism, always make base class destructors virtual to ensure proper cleanup:

C++
class Base {
public:
    virtual ~Base() {  // Virtual destructor
        cout << "Base destructor" << endl;
    }
};

class Derived : public Base {
private:
    int* data;
    
public:
    Derived() {
        data = new int[100];
    }
    
    ~Derived() {
        delete[] data;
        cout << "Derived destructor" << endl;
    }
};

int main() {
    Base* ptr = new Derived();
    delete ptr;  // Calls both destructors thanks to virtual
}

3. Use the override Keyword

Always use override when overriding virtual functions to catch errors:

C++
class Base {
public:
    virtual void process(int x) {
        cout << "Base process" << endl;
    }
};

class Derived : public Base {
public:
    // Typo caught by override keyword
    void process(double x) override {  // Error! Doesn't match base signature
        cout << "Derived process" << endl;
    }
};

4. Follow the Liskov Substitution Principle

Derived classes should be substitutable for their base classes without breaking functionality:

C++
// Good - Square IS-A Rectangle semantically but violates LSP
class Rectangle {
protected:
    double width, height;
public:
    virtual void setWidth(double w) { width = w; }
    virtual void setHeight(double h) { height = h; }
    double getArea() { return width * height; }
};

// Better approach - separate hierarchy or different design
class Shape {
public:
    virtual double getArea() = 0;
};

class Rectangle : public Shape {
    // Independent width and height
};

class Square : public Shape {
    // Single side dimension
};

Advanced Inheritance Topics

Final Classes and Methods

The final keyword prevents further inheritance or overriding:

C++
class Base {
public:
    virtual void cannotOverride() final {
        cout << "This method cannot be overridden" << endl;
    }
    
    virtual void canOverride() {
        cout << "This method can be overridden" << endl;
    }
};

class FinalClass final : public Base {
    // This class cannot be inherited from
};

// class WontWork : public FinalClass {  // Error!
// };

Using Base Class Pointers for Polymorphism

Store different derived class objects in base class pointers for flexible code:

C++
void processShapes(Shape* shapes[], int count) {
    for (int i = 0; i < count; i++) {
        shapes[i]->draw();
        cout << "Area: " << shapes[i]->getArea() << endl;
    }
}

int main() {
    Shape* shapes[3];
    shapes[0] = new Circle("red", 5);
    shapes[1] = new Rectangle("blue", 4, 6);
    shapes[2] = new Triangle("green", 3, 4, 5);
    
    processShapes(shapes, 3);
    
    for (int i = 0; i < 3; i++) {
        delete shapes[i];
    }
}

Conclusion: Building Robust Class Hierarchies

Inheritance is one of C++’s most powerful features, enabling you to create sophisticated, maintainable software architectures. Through proper use of base and derived classes, you can build systems where common functionality is shared, specialized behavior is localized, and code reuse is maximized.

The key principles to remember:

  • Use public inheritance to model true “is-a” relationships
  • Make destructors virtual in base classes when using polymorphism
  • Leverage protected members to share functionality with derived classes while maintaining encapsulation
  • Override virtual functions to customize behavior in derived classes
  • Use abstract classes to define interfaces that derived classes must implement
  • Consider composition when inheritance doesn’t represent a true “is-a” relationship
  • Follow the Liskov Substitution Principle to ensure derived classes can truly substitute for base classes

Whether you’re building a simple two-level hierarchy or a complex multi-level inheritance structure, the principles remain the same: create logical relationships, maintain encapsulation, and design for extension. With practice, you’ll develop an intuition for when inheritance is the right tool and how to structure your hierarchies for maximum clarity and flexibility.

Mastering inheritance opens the door to advanced C++ techniques like polymorphism, abstract interfaces, and design patterns. It transforms your code from a collection of independent classes into a cohesive, well-organized system that mirrors the logical relationships in your problem domain. This is the essence of object-oriented design, and it’s what makes C++ such a powerful language for building large-scale software systems.

Share:
Subscribe
Notify of
0 Comments
Inline Feedbacks
View all comments

Discover More

Understanding Break and Continue in Loops

Master C++ break and continue statements for loop control. Learn when to exit loops early,…

Clicks Introduces Retro-Style Smartphone and Keyboard at CES 2026

Clicks announced its first smartphone featuring a tactile keyboard and messaging focus at CES 2026,…

Understanding Voltage: The Driving Force of Electronics

Explore the critical role of voltage in electronics, from powering devices to enabling advanced applications…

Understanding the Difference Between AI, Machine Learning, and Deep Learning

Understand the differences between AI, machine learning, and deep learning. Learn how these technologies relate,…

Introduction to Jupyter Notebooks for AI Experimentation

Master Git and GitHub for AI and machine learning projects. Learn version control fundamentals, branching,…

Why Arduino Changed Hobbyist Robotics Forever

Discover how Arduino revolutionized hobbyist robotics by making microcontrollers accessible to everyone. Learn why Arduino…

Click For More
0
Would love your thoughts, please comment.x
()
x