Chapter 10 of 23

Inheritance in C++

Learn single, multilevel, and multiple inheritance, access control in inheritance, constructor/destructor order, the is-a vs has-a distinction, and a Vehicle hierarchy worked example.

Meritshot12 min read
C++InheritanceOOPVirtual FunctionsPolymorphismBase Class
All C++ Chapters

What Is Inheritance?

Inheritance is the OOP mechanism that lets a new class (the derived class or child class) acquire the properties and behaviour of an existing class (the base class or parent class). The derived class can extend or customise the base class without modifying it.

Inheritance models the "is-a" relationship:

  • A Car is a Vehicle
  • A SavingsAccount is a BankAccount
  • A Dog is an Animal

This hierarchy reduces code duplication, makes extensions easier, and is a core topic in every C++ interview round at TCS, Infosys, Google India, Amazon, and Microsoft.


Single Inheritance

Single inheritance — one derived class, one base class — is the most common form.

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

// Base class
class Vehicle {
protected:
    string brand;
    int    year;

public:
    Vehicle(string b, int y) : brand(b), year(y) {}

    void displayInfo() const {
        cout << brand << " (" << year << ")\n";
    }

    void startEngine() const {
        cout << brand << ": Engine started\n";
    }
};

// Derived class
class Car : public Vehicle {
private:
    int numDoors;

public:
    Car(string b, int y, int doors)
        : Vehicle(b, y), numDoors(doors) {}  // call base constructor

    void displayCarInfo() const {
        displayInfo();                        // inherited method
        cout << "Doors: " << numDoors << "\n";
    }
};

int main() {
    Car c("Maruti Swift", 2023, 4);
    c.startEngine();       // inherited from Vehicle
    c.displayCarInfo();
    return 0;
}

Output:

Maruti Swift: Engine started
Maruti Swift (2023)
Doors: 4

The derived class inherits all non-private members of the base class. The base class constructor is called first, via the initialiser list.


Access Specifiers in the Base Class

Recall from the previous chapter that member access in the base class affects what the derived class sees:

Base member visibilitypublic inheritanceprotected inheritanceprivate inheritance
public in basepublic in derivedprotected in derivedprivate in derived
protected in baseprotected in derivedprotected in derivedprivate in derived
private in baseNot accessibleNot accessibleNot accessible

public inheritance (class Derived : public Base) is by far the most common — it preserves the base class interface. Use protected or private inheritance only for specialised implementation-detail scenarios.


Multilevel Inheritance

Multilevel inheritance creates a chain: A derives from B, and B derives from C. The chain can be as long as needed, though deep hierarchies become hard to maintain.

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

class LivingBeing {
public:
    void breathe() const {
        cout << "Breathing...\n";
    }
};

class Animal : public LivingBeing {
public:
    void eat() const {
        cout << "Eating...\n";
    }
};

class Dog : public Animal {
public:
    void bark() const {
        cout << "Woof!\n";
    }
};

int main() {
    Dog d;
    d.breathe();  // from LivingBeing
    d.eat();      // from Animal
    d.bark();     // own method
    return 0;
}

Dog inherits from Animal, which inherits from LivingBeing. So Dog objects have access to all three levels' public and protected members.


Multiple Inheritance

Multiple inheritance lets a derived class inherit from more than one base class simultaneously. C++ supports this; Java and Python (partially) do not.

#include <iostream>
using namespace std;

class Flyable {
public:
    void fly() const {
        cout << "Flying...\n";
    }
};

class Swimmable {
public:
    void swim() const {
        cout << "Swimming...\n";
    }
};

class Duck : public Flyable, public Swimmable {
public:
    void quack() const {
        cout << "Quack!\n";
    }
};

int main() {
    Duck d;
    d.fly();    // from Flyable
    d.swim();   // from Swimmable
    d.quack();  // own method
    return 0;
}

The Diamond Problem

Multiple inheritance can cause ambiguity when two base classes both inherit from the same grandparent — the diamond problem. C++ resolves this with virtual inheritance:

class A { public: int value = 10; };

class B : virtual public A {};
class C : virtual public A {};

class D : public B, public C {};  // only ONE copy of A::value

int main() {
    D obj;
    cout << obj.value << "\n";  // unambiguous: 10
    return 0;
}

Without virtual, D would contain two copies of A::value, and obj.value would be a compile error.


Constructor and Destructor Call Order

In an inheritance chain, constructors are called base-first, derived-last. Destructors are called in the reverse order — derived-first, base-last.

#include <iostream>
using namespace std;

class A {
public:
    A()  { cout << "A constructed\n"; }
    ~A() { cout << "A destroyed\n";   }
};

class B : public A {
public:
    B()  { cout << "B constructed\n"; }
    ~B() { cout << "B destroyed\n";   }
};

class C : public B {
public:
    C()  { cout << "C constructed\n"; }
    ~C() { cout << "C destroyed\n";   }
};

int main() {
    C obj;
    return 0;
}

Output:

A constructed
B constructed
C constructed
C destroyed
B destroyed
A destroyed

This order ensures that when a derived constructor runs, the base part is already fully initialised.


Passing Arguments to the Base Constructor

Use the derived constructor's initialiser list to pass arguments up the chain:

class Vehicle {
protected:
    string brand;
    int    year;
public:
    Vehicle(string b, int y) : brand(b), year(y) {}
};

class Car : public Vehicle {
private:
    int numDoors;
public:
    Car(string b, int y, int d)
        : Vehicle(b, y), numDoors(d) {}   // forward b and y to Vehicle
};

class ElectricCar : public Car {
private:
    int rangeKm;
public:
    ElectricCar(string b, int y, int d, int r)
        : Car(b, y, d), rangeKm(r) {}     // forward b, y, d to Car
};

The virtual Keyword — A Preview

When a base class pointer points to a derived class object, calling a method normally invokes the base class version of that method. Adding virtual to the base class method enables dynamic dispatch — the correct derived-class version is called at runtime.

class Shape {
public:
    virtual double area() const {
        return 0.0;
    }
};

class Circle : public Shape {
    double radius;
public:
    Circle(double r) : radius(r) {}
    double area() const override {
        return 3.14159 * radius * radius;
    }
};

int main() {
    Shape* s = new Circle(5.0);
    cout << s->area() << "\n";  // 78.54 — calls Circle::area, not Shape::area
    delete s;
    return 0;
}

Virtual functions are the mechanism behind polymorphism and are covered in depth in the next chapter.


is-a vs has-a

Choosing between inheritance and composition is one of the most important design decisions in OOP.

RelationshipMeaningImplement with
is-aDerived IS a kind of basepublic inheritance
has-aClass HAS a member of another typeComposition (member variable)
// is-a: A Tata Nexon IS a Car
class Car : public Vehicle { ... };

// has-a: A Car HAS an Engine (not "is an Engine")
class Car {
private:
    Engine engine;   // composition
    ...
};

A common mistake is to use inheritance just to reuse code when the relationship is actually "has-a". Prefer composition over inheritance when in doubt — the relationship should make semantic sense.


Worked Example: VehicleCarElectricCar Hierarchy

This hierarchy mirrors real-world automotive classification — relevant context as India's EV market expands with companies like Tata Motors, Ola Electric, and Ather Energy.

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

// ────────────────────────────────────────────
// Level 1: Base class
// ────────────────────────────────────────────
class Vehicle {
protected:
    string brand;
    string model;
    int    year;
    double priceINR;

public:
    Vehicle(string b, string m, int y, double price)
        : brand(b), model(m), year(y), priceINR(price) {}

    void displayBasicInfo() const {
        cout << year << " " << brand << " " << model
             << " | Price: Rs. " << priceINR << "\n";
    }

    void startEngine() const {
        cout << brand << " " << model << ": Engine started\n";
    }

    virtual ~Vehicle() {
        cout << "[Vehicle] " << brand << " " << model << " scrapped\n";
    }
};

// ────────────────────────────────────────────
// Level 2: Car (inherits Vehicle)
// ────────────────────────────────────────────
class Car : public Vehicle {
protected:
    int    numDoors;
    string fuelType;
    double engineCC;

public:
    Car(string b, string m, int y, double price,
        int doors, string fuel, double cc)
        : Vehicle(b, m, y, price),
          numDoors(doors), fuelType(fuel), engineCC(cc) {}

    void displayCarInfo() const {
        displayBasicInfo();
        cout << "  Doors: "  << numDoors
             << " | Fuel: "  << fuelType
             << " | CC: "    << engineCC << "\n";
    }

    bool canTow() const {
        return engineCC >= 1500.0;
    }

    virtual ~Car() {
        cout << "[Car] " << brand << " " << model << " deregistered\n";
    }
};

// ────────────────────────────────────────────
// Level 3: ElectricCar (inherits Car)
// ────────────────────────────────────────────
class ElectricCar : public Car {
private:
    int    batteryKWh;
    int    rangeKm;
    int    chargingTimeMins;
    bool   fastChargeSupported;

public:
    ElectricCar(string b, string m, int y, double price,
                int doors,
                int battery, int range, int chargeTime, bool fastCharge)
        : Car(b, m, y, price, doors, "Electric", 0.0),
          batteryKWh(battery), rangeKm(range),
          chargingTimeMins(chargeTime),
          fastChargeSupported(fastCharge) {}

    void displayEVInfo() const {
        displayCarInfo();
        cout << "  Battery: "  << batteryKWh   << " kWh"
             << " | Range: "   << rangeKm       << " km"
             << " | Charge: "  << chargingTimeMins << " min"
             << " | Fast Charge: " << (fastChargeSupported ? "Yes" : "No")
             << "\n";
    }

    double costPer100Km(double electricityRatePerUnit) const {
        // kWh per 100 km = (batteryKWh / rangeKm) * 100
        double unitsNeeded = ((double)batteryKWh / rangeKm) * 100.0;
        return unitsNeeded * electricityRatePerUnit;
    }

    virtual ~ElectricCar() {
        cout << "[ElectricCar] " << brand << " " << model
             << " battery recycled\n";
    }
};

// ────────────────────────────────────────────
// Main
// ────────────────────────────────────────────
int main() {
    cout << "=== Petrol Car ===\n";
    Car swift("Maruti Suzuki", "Swift", 2024, 899000.0,
              4, "Petrol", 1197.0);
    swift.startEngine();
    swift.displayCarInfo();
    cout << "Can tow: " << (swift.canTow() ? "Yes" : "No") << "\n";

    cout << "\n=== Electric Car ===\n";
    ElectricCar nexon("Tata", "Nexon EV Max", 2024, 1999000.0,
                      4, 40, 437, 56, true);
    nexon.startEngine();
    nexon.displayEVInfo();

    double electricityRate = 8.0;  // Rs. 8 per kWh (typical Indian rate)
    cout << "Running cost per 100 km: Rs. "
         << nexon.costPer100Km(electricityRate) << "\n";

    cout << "\n=== Object Lifetimes ===\n";
    return 0;   // destructors called here
}

Sample Output:

=== Petrol Car ===
Maruti Suzuki Swift: Engine started
2024 Maruti Suzuki Swift | Price: Rs. 899000
  Doors: 4 | Fuel: Petrol | CC: 1197
Can tow: No

=== Electric Car ===
Tata Nexon EV Max: Engine started
2024 Tata Nexon EV Max | Price: Rs. 1999000
  Doors: 4 | Fuel: Electric | CC: 0
  Battery: 40 kWh | Range: 437 km | Charge: 56 min | Fast Charge: Yes
Running cost per 100 km: Rs. 7.32...

=== Object Lifetimes ===
[ElectricCar] Tata Nexon EV Max battery recycled
[Car] Tata Nexon EV Max deregistered
[Vehicle] Tata Nexon EV Max scrapped
[Car] Maruti Suzuki Swift deregistered
[Vehicle] Maruti Suzuki Swift scrapped

Notice the destructor chain: ElectricCarCarVehicle — the reverse of the constructor order. Making the destructors virtual ensures the correct chain fires even when deleting through a base-class pointer.


Common Pitfalls

  • Non-virtual destructor in a base class: If you delete a derived object through a base pointer and the destructor is not virtual, only the base destructor runs. Resource leaks and double-frees follow. Always make base class destructors virtual.
  • Forgetting to call the base constructor: If you do not explicitly call the base constructor in the derived initialiser list, the default base constructor is called. If the base has no default constructor, you get a compile error.
  • Overusing inheritance: Not every code-reuse situation calls for inheritance. Use composition ("has-a") when the relationship is not genuinely "is-a".
  • Hiding base class methods: If a derived class defines a function with the same name but a different signature, it hides all overloads in the base. Use using Base::method; to bring them back.
  • The diamond problem without virtual inheritance: Two base classes both inheriting from the same grandparent leads to ambiguity. Resolve it with virtual public inheritance.
  • Calling virtual functions in constructors: During base class construction, the virtual table is not fully populated yet. Calling a virtual function from a constructor invokes the base version, not the overridden one.

Practice Exercises

  1. Create a Shape base class with area() and perimeter() methods. Derive Circle, Rectangle, and Triangle from it. Instantiate all three and print their areas.
  2. Build a PersonEmployeeManager three-level hierarchy. Person has name and age; Employee adds salary and company; Manager adds teamSize. Print a summary for a Manager object, showing all inherited and own fields.
  3. Implement multiple inheritance: classes Readable and Writable each have one relevant method. Create a File class that inherits from both and demonstrate calling both methods.
  4. Demonstrate the diamond problem: create a class hierarchy DeviceScanner, DevicePrinterAllInOne (from both Scanner and Printer). Fix the ambiguity with virtual inheritance.
  5. Extend the ElectricCar class with a charge(int minutes) method that estimates how many km of range are added. Override the startEngine() method (hint: EVs do not have engines — print "Motor activated" instead).

Summary

  • Inheritance models the "is-a" relationship: a derived class acquires the properties and behaviour of its base class.
  • Single inheritance (one base, one derived) is the most common form.
  • Multilevel inheritance chains classes: grandparent → parent → child.
  • Multiple inheritance lets a class inherit from more than one base; the diamond problem is resolved with virtual inheritance.
  • Access in the derived class depends on both the member's access in the base and the inheritance specifier (public, protected, or private).
  • Constructors run base-first, derived-last; destructors run in the reverse order.
  • Always declare base class destructors virtual to ensure correct cleanup when deleting derived objects through base pointers.
  • The virtual keyword on a method enables runtime polymorphism — the correct override is called even through a base pointer.
  • Prefer composition ("has-a") over inheritance when the semantic "is-a" relationship does not hold naturally.
  • The VehicleCarElectricCar example demonstrated all constructor/destructor ordering, method inheritance, and three-level hierarchy design in a realistic Indian automotive context.