C++ Inheritance

Inheritance lets one class (the derived class) reuse and extend the members of another class (the base class). Instead of writing the same fields and functions over and over, you express an “is-a” relationship: a Dog is an Animal, a Manager is an Employee. Inheritance is one of the three pillars of object-oriented programming in C++, alongside encapsulation and polymorphism, and it is the mechanism that makes polymorphism possible in the first place.

In this lesson you will learn how inheritance actually works under the hood, the different access levels you can inherit with, how constructors and destructors run across a hierarchy, multilevel and multiple inheritance, and the mistakes that trip up almost every beginner (and more than a few experienced developers).

Overview / How Inheritance Works

When you declare class Dog : public Animal, the compiler makes every non-private member of Animal part of Dog. Conceptually, a Dog object contains a complete Animal object as its first “slice,” followed by whatever new members Dog adds. This is not just a logical idea — it is reflected in memory layout: the base class subobject occupies the first bytes of the derived object, and derived-only members come after it. A pointer to a Dog can always be implicitly converted to a pointer to Animal, because the Animal part really does live at the start of the object.

Every class member has an access level — public, protected, or private — and inheritance itself also has an access level, written after the colon in the class declaration. The inheritance access level controls how the base class’s public and protected members appear to code outside the hierarchy:

Inheritance type public members become protected members become private members
public public protected never accessible in derived class
protected protected protected never accessible in derived class
private private private never accessible in derived class

A base class’s private members are never directly visible in a derived class, no matter which inheritance type you use — they still exist inside the derived object and are still initialized, but the derived class must go through the base class’s public or protected interface to touch them. This is why protected exists: it is like private to the outside world, but visible to derived classes.

Almost all real-world inheritance uses public inheritance, because it models a true “is-a” relationship where a derived object can be used anywhere a base object is expected (this is called the Liskov Substitution Principle). protected and private inheritance are rare and model “implemented-in-terms-of” relationships instead — most C++ style guides recommend composition (having a member object) over private inheritance for that case.

Syntax

class Derived : access-specifier Base {
    // derived class members
};
  • Derived — the name of the new class being defined.
  • access-specifierpublic, protected, or private; controls how inherited members are exposed (defaults to private for class, public for struct, if omitted).
  • Base — the name of the existing class being inherited from.
  • A derived class can list multiple base classes separated by commas for multiple inheritance: class D : public A, public B { ... };
  • Derived-class constructors call a base-class constructor using a member initializer list: Derived(args) : Base(args) { ... }. If you omit this, the base class’s default constructor runs automatically — and if the base has no default constructor, this is a compile error.

Examples

Example 1: Basic public inheritance

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

class Animal {
protected:
    string name;
public:
    Animal(const string& n) : name(n) {}
    void eat() const {
        cout << name << " is eating." << endl;
    }
};

class Dog : public Animal {
public:
    Dog(const string& n) : Animal(n) {}
    void bark() const {
        cout << name << " says: Woof!" << endl;
    }
};

int main() {
    Dog myDog("Rex");
    myDog.eat();
    myDog.bark();
    return 0;
}

Output:

Rex is eating.
Rex says: Woof!

Dog does not redeclare name or eat() — it inherits them directly from Animal. Because name is protected rather than private, Dog::bark() can read it directly. The Dog constructor forwards its argument to Animal‘s constructor through the initializer list.

Example 2: Multilevel inheritance and construction order

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

class Person {
protected:
    string name;
public:
    Person(const string& n) : name(n) {
        cout << "Person constructor: " << name << endl;
    }
    ~Person() {
        cout << "Person destructor: " << name << endl;
    }
};

class Employee : public Person {
protected:
    double salary;
public:
    Employee(const string& n, double s) : Person(n), salary(s) {
        cout << "Employee constructor: " << name << endl;
    }
    ~Employee() {
        cout << "Employee destructor: " << name << endl;
    }
};

class Manager : public Employee {
    int teamSize;
public:
    Manager(const string& n, double s, int team)
        : Employee(n, s), teamSize(team) {
        cout << "Manager constructor: " << name << endl;
    }
    ~Manager() {
        cout << "Manager destructor: " << name << endl;
    }
    void describe() const {
        cout << name << " manages " << teamSize
             << " people and earns $" << salary << endl;
    }
};

int main() {
    {
        Manager m("Alice", 95000.0, 6);
        m.describe();
    }
    return 0;
}

Output:

Person constructor: Alice
Employee constructor: Alice
Manager constructor: Alice
Alice manages 6 people and earns $95000
Manager destructor: Alice
Employee destructor: Alice
Person destructor: Alice

Manager inherits from Employee, which inherits from Person — a three-level chain. Construction always runs base-first, most-derived-last, and destruction runs in the exact opposite order. This guarantees that a derived class’s constructor can always rely on its base subobjects already being fully built, and a base destructor never runs on an object whose derived parts still exist.

Example 3: Multiple inheritance

#include <iostream>
using namespace std;

class Flyable {
public:
    void fly() const {
        cout << "Flying through the air." << endl;
    }
};

class Swimmable {
public:
    void swim() const {
        cout << "Swimming through water." << endl;
    }
};

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

int main() {
    Duck donald;
    donald.fly();
    donald.swim();
    donald.quack();
    return 0;
}

Output:

Flying through the air.
Swimming through water.
Quack!

Duck inherits from two unrelated base classes at once, combining their capabilities. Multiple inheritance is powerful for composing independent behaviors like this, but it can get complicated fast: if two base classes shared a common ancestor, or defined a member with the same name, you would run into ambiguity that requires virtual inheritance or explicit scope resolution (Base::member) to fix. Use multiple inheritance sparingly and prefer it for small, behavior-only base classes.

How It Works Step by Step / Under the Hood

  • Memory layout: a derived object physically contains its base subobject(s) followed by its own new members. sizeof(Derived) is at least sizeof(Base) plus the size of the derived-only members (plus padding).
  • Construction order: the compiler always builds base classes first (in the order they are listed after the colon, not the order of the initializer list), then member variables in declaration order, then runs the derived constructor’s body.
  • Destruction order: exactly reversed — derived destructor body runs first, then members are destroyed, then base destructors run last.
  • Pointer conversion: a Derived* converts to a Base* for free (upcasting), because the base subobject’s address is the start of the derived object (in the common single-inheritance case). This is what makes polymorphism through base-class pointers/references possible.
  • No virtual functions needed for inheritance itself: inheritance alone just gives you code reuse and the is-a relationship. Runtime polymorphism (calling the derived version of a function through a base pointer) requires marking functions virtual, which is covered in the next lesson.

Common Mistakes

Mistake 1: Object slicing

Passing a derived object by value into a function or container that expects the base type “slices off” the derived part, silently discarding data and defeating any polymorphic behavior.

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

class Base {
public:
    string tag = "Base";
    virtual void show() const { cout << "Base: " << tag << endl; }
};

class Derived : public Base {
public:
    string extra = "Derived-only data";
    void show() const override { cout << "Derived: " << tag << " + " << extra << endl; }
};

void printIt(Base b) {
    b.show();
}

int main() {
    Derived d;
    printIt(d);
    return 0;
}

Output:

Base: Base

Even though show() is virtual and d is a Derived, passing it by value into printIt(Base b) copies only the Base portion into a brand-new Base object — extra is lost and b.show() calls Base::show. The fix is to take a reference or pointer instead: void printIt(const Base& b) avoids the copy entirely and preserves the dynamic type.

Mistake 2: Forgetting a virtual destructor

If a base class will ever be deleted through a base-class pointer, its destructor must be virtual. Otherwise, only the base destructor runs, and any resources owned by the derived part leak.

#include <iostream>
using namespace std;

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

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

int main() {
    Base* ptr = new Derived();
    delete ptr;
    return 0;
}

Output:

Base destructor

Notice Derived destructor never prints, and the data array is never freed — a memory leak. Because Base‘s destructor is not virtual, the compiler resolves delete ptr using the static type (Base*) rather than the actual object type. The rule of thumb: if a class has any virtual function, or is ever meant to be deleted polymorphically, give it a virtual destructor.

Best Practices

  • Use public inheritance to model genuine “is-a” relationships; prefer composition (“has-a”, a member object) for everything else.
  • Mark base-class members that derived classes need direct access to as protected, not public — keep the truly external interface as small as possible.
  • Give any class intended as a polymorphic base a virtual destructor, even if it is otherwise empty.
  • Pass and store base-class objects by reference or pointer, never by value, to avoid object slicing.
  • Use the override keyword on every function meant to override a base virtual function so the compiler catches signature mismatches.
  • Keep inheritance hierarchies shallow (2–3 levels); deep hierarchies become hard to reason about and modify safely.
  • Be cautious with multiple inheritance — it is most useful for small, focused “capability” classes, not for combining large, stateful classes.

Practice Exercises

  • Exercise 1: Create a base class Shape with a protected string name and a method describe() that prints the name. Derive Circle and Rectangle classes, each adding their own dimension fields and an area() method. Construct one of each and print their descriptions and areas.
  • Exercise 2: Build a three-level hierarchy VehicleCarElectricCar, each adding one new protected member and printing a message from its constructor and destructor. Create an ElectricCar inside a nested scope and observe the construction/destruction order in the output.
  • Exercise 3: Write a base class Resource that allocates an array with new in its constructor and frees it in its destructor, but deliberately leave the destructor non-virtual. Derive a class, allocate a Derived*, store it in a Resource*, and delete it — confirm the derived destructor is skipped. Then fix the bug by making the base destructor virtual and confirm both destructors run.

Summary

  • Inheritance lets a derived class reuse and extend a base class’s members, modeling an “is-a” relationship.
  • A derived object physically contains its base subobject(s); base members become part of the derived object’s memory layout.
  • public, protected, and private inheritance control how inherited members are exposed to the outside world; public is by far the most common.
  • Constructors run base-first and destructors run base-last, guaranteeing bases are fully built before derived code runs and fully intact while derived code tears down.
  • C++ supports multilevel inheritance (a chain of derived classes) and multiple inheritance (a class with more than one direct base).
  • Object slicing and missing virtual destructors are the two most common inheritance bugs — use references/pointers to base classes and give polymorphic bases virtual destructors.