Home C / C++ C++ Inheritance Explained — Types, Pitfalls and Real-World Patterns

C++ Inheritance Explained — Types, Pitfalls and Real-World Patterns

In Plain English 🔥
Think of inheritance like a family recipe book. Grandma has a master recipe for 'cake' — she knows the basics: flour, eggs, sugar, oven temperature. Your mum inherits that recipe and adds chocolate chips. You inherit your mum's version and swap in oat flour. Nobody rewrote the whole book from scratch. That's exactly what inheritance does in C++ — a new class automatically gets everything a parent class already knows, and then adds its own twist on top.
⚡ Quick Answer
Think of inheritance like a family recipe book. Grandma has a master recipe for 'cake' — she knows the basics: flour, eggs, sugar, oven temperature. Your mum inherits that recipe and adds chocolate chips. You inherit your mum's version and swap in oat flour. Nobody rewrote the whole book from scratch. That's exactly what inheritance does in C++ — a new class automatically gets everything a parent class already knows, and then adds its own twist on top.

Every large C++ codebase you'll ever work in — game engines, OS drivers, financial systems — uses inheritance somewhere. It's not a fancy academic trick. It's the practical answer to a real problem: when multiple things in your program share common behaviour, you shouldn't copy-paste that behaviour into each one. Code duplication is the silent killer of maintainability, and inheritance is one of the sharpest tools for defeating it.

Without inheritance, you'd write a 'save to disk' function separately for every class that needs it — User, Order, Product, Session. Change one detail of how saving works, and you're hunting through five files making the same edit. Inheritance lets you write that logic once in a base class and have every derived class pick it up automatically. It also enables polymorphism — the ability to treat different objects through a common interface — which is what makes plugin architectures, game entity systems, and GUI frameworks possible.

By the end of this article you'll be able to model real inheritance hierarchies yourself, understand the critical difference between public and private inheritance, dodge the two bugs that catch almost every intermediate C++ developer off guard, and answer the inheritance questions that come up in system-design interviews. Let's build something real.

Single Inheritance — Building Your First Base Class

Single inheritance is the simplest form: one class derives from exactly one parent. The derived class gets all public and protected members of the base class as if they were its own. Private members still exist in memory but are off-limits directly — they're the base class's internal secrets.

Why does the distinction between public, protected, and private members matter here? Because it defines the contract. Public members are the class's API — things it promises the outside world. Protected members are shared within the family but hidden from strangers. Private members belong to this class alone, full stop.

Here's a concrete scenario: you're building a simple vehicle management system. Every vehicle has a make, model, and a way to display its details. A Car adds the number of doors. Instead of writing the make/model logic twice, you put it in a base Vehicle class and let Car inherit it.

SingleInheritance.cpp · CPP
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
#include <iostream>
#include <string>

// Base class — contains everything common to ALL vehicles
class Vehicle {
public:
    std::string make;
    std::string model;
    int year;

    // Constructor: initialises the core vehicle data
    Vehicle(const std::string& make, const std::string& model, int year)
        : make(make), model(model), year(year) {}

    // Any vehicle can display its basic identity
    void displayInfo() const {
        std::cout << year << " " << make << " " << model;
    }
};

// Derived class — inherits Vehicle publicly, then extends it
class Car : public Vehicle {
public:
    int numberOfDoors;

    // We call Vehicle's constructor via the initialiser list
    // — the base part is constructed FIRST, always
    Car(const std::string& make, const std::string& model,
        int year, int doors)
        : Vehicle(make, model, year), numberOfDoors(doors) {}

    // Car-specific display — reuses the base method then adds its own info
    void displayCarInfo() const {
        displayInfo();  // inherited from Vehicle — no duplication needed
        std::cout << " | Doors: " << numberOfDoors << "\n";
    }
};

int main() {
    Vehicle genericVehicle("Generic", "Transport", 2020);
    genericVehicle.displayInfo();
    std::cout << "\n";

    Car familyCar("Toyota", "Camry", 2023, 4);
    familyCar.displayCarInfo();

    // Car inherits make/model/year directly — no extra code needed
    std::cout << "Make accessed directly: " << familyCar.make << "\n";

    return 0;
}
▶ Output
2020 Generic Transport
2023 Toyota Camry | Doors: 4
Make accessed directly: Toyota
⚠️
Pro Tip:Always initialise the base class through the member initialiser list (`: Vehicle(make, model, year)`), never inside the constructor body. The base class constructor MUST run before any derived class logic, and the initialiser list is how you control which base constructor gets called and with which arguments.

Public vs Protected vs Private Inheritance — The Bit Everyone Gets Wrong

When you write class Car : public Vehicle, that public keyword isn't about Vehicle's members — it's about how those members are re-exposed through Car to the outside world. This is the single most misunderstood concept in C++ inheritance, and getting it wrong leads to bizarre access errors.

Here's the mental model: the inheritance access specifier acts like a filter that can only make things more restrictive, never less. With public inheritance, public stays public and protected stays protected. With protected inheritance, public members become protected in the derived class. With private inheritance, everything from the base becomes private — completely locked away.

public inheritance models an IS-A relationship: a Car IS-A Vehicle. This is the form you'll use 90% of the time. private inheritance models HAS-A or IMPLEMENTED-IN-TERMS-OF — it's an implementation detail, not a conceptual relationship. If you catch yourself writing private inheritance, ask whether composition (just storing a member) would be cleaner.

InheritanceAccessDemo.cpp · CPP
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
#include <iostream>
#include <string>

class Engine {
public:
    void start() { std::cout << "Engine started\n"; }
    void stop()  { std::cout << "Engine stopped\n"; }
protected:
    int horsepower = 150;
private:
    std::string serialNumber = "ENG-XZ99"; // nobody outside Engine touches this
};

// PUBLIC inheritance — Car IS-A Engine (conceptually makes sense to expose API)
class PublicCar : public Engine {
public:
    void drive() {
        start();             // OK — public inherited, stays public
        std::cout << "Driving with " << horsepower << " HP\n"; // OK — protected
        // serialNumber;     // COMPILE ERROR — private is always off-limits
    }
};

// PRIVATE inheritance — ElectricBooster uses Engine internally
// The world outside ElectricBooster cannot call start() or stop()
class ElectricBooster : private Engine {
public:
    void activate() {
        start();  // OK inside the class — but outsiders can't call booster.start()
        std::cout << "Booster running at " << horsepower << " HP\n";
    }
};

int main() {
    PublicCar sportsCar;
    sportsCar.start();   // Fine — public inheritance keeps it public
    sportsCar.drive();
    sportsCar.stop();

    std::cout << "---\n";

    ElectricBooster booster;
    booster.activate();  // Fine — activate() is public
    // booster.start();  // COMPILE ERROR — start() is now private via private inheritance

    return 0;
}
▶ Output
Engine started
Driving with 150 HP
Engine stopped
---
Engine started
Booster running at 150 HP
⚠️
Watch Out:The default inheritance in C++ is PRIVATE for `class` and PUBLIC for `struct`. If you write `class Car : Vehicle` (no specifier), it's private inheritance — which is almost never what you want. Always write the access specifier explicitly so your intent is clear and reviewers don't have to guess.

Virtual Functions and Polymorphism — Where Inheritance Gets Its Superpower

Inheriting data and functions is useful, but inheritance really earns its keep when you combine it with virtual functions. A virtual function says: 'I'm providing a default, but derived classes can replace me — and the RIGHT version will be called at runtime based on what the object actually is, not what pointer type you're using.'

This is runtime polymorphism — one of the core pillars of object-oriented design. Without it, you'd need a giant switch statement to check object types and call the right method. With it, you write code against the base class pointer and the system figures out which derived implementation to invoke automatically.

The virtual keyword on the base method opts into this mechanism. The override keyword on derived methods (C++11 onwards) makes the compiler confirm you're actually overriding something — it catches typos in function signatures that would otherwise silently create a new, unrelated function. Always use override. The = 0 syntax makes a function pure virtual, turning the class into an abstract base — it can't be instantiated and forces every derived class to provide an implementation.

PolymorphismDemo.cpp · CPP
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
#include <iostream>
#include <vector>
#include <memory>
#include <string>

// Abstract base class — cannot be instantiated directly
// Any class that forgets to implement fuelCost() won't compile
class Vehicle {
public:
    std::string make;
    int milesDriven;

    Vehicle(const std::string& make, int miles)
        : make(make), milesDriven(miles) {}

    // Pure virtual — every vehicle MUST define how fuel cost is calculated
    virtual double fuelCost() const = 0;

    // Virtual with a default — derived classes CAN override but don't have to
    virtual std::string fuelType() const {
        return "Unknown";
    }

    // Non-virtual: this behaviour should NEVER change per subtype
    void printSummary() const {
        std::cout << make
                  << " | Fuel: " << fuelType()
                  << " | Cost for " << milesDriven
                  << " miles: $" << fuelCost() << "\n";
    }

    // ALWAYS declare the destructor virtual in a polymorphic base class
    // (see Gotchas section — this one bites hard)
    virtual ~Vehicle() = default;
};

class PetrolCar : public Vehicle {
    double milesPerGallon;
    double pricePerGallon;
public:
    PetrolCar(const std::string& make, int miles,
              double mpg, double pricePerGallon)
        : Vehicle(make, miles),
          milesPerGallon(mpg),
          pricePerGallon(pricePerGallon) {}

    // override keyword tells the compiler: "check that I'm actually overriding"
    double fuelCost() const override {
        return (milesDriven / milesPerGallon) * pricePerGallon;
    }

    std::string fuelType() const override { return "Petrol"; }
};

class ElectricCar : public Vehicle {
    double milesPerKWh;
    double pricePerKWh;
public:
    ElectricCar(const std::string& make, int miles,
                double milesPerKWh, double pricePerKWh)
        : Vehicle(make, miles),
          milesPerKWh(milesPerKWh),
          pricePerKWh(pricePerKWh) {}

    double fuelCost() const override {
        return (milesDriven / milesPerKWh) * pricePerKWh;
    }

    std::string fuelType() const override { return "Electric"; }
};

int main() {
    // Store different vehicle types through a common base pointer
    // This is the power: the vector doesn't care which subtype it holds
    std::vector<std::unique_ptr<Vehicle>> fleet;

    fleet.push_back(std::make_unique<PetrolCar>("Ford F-150", 300, 20.0, 3.50));
    fleet.push_back(std::make_unique<ElectricCar>("Tesla Model 3", 300, 4.0, 0.13));
    fleet.push_back(std::make_unique<PetrolCar>("Honda Civic", 300, 35.0, 3.50));

    std::cout << "=== Fleet Cost Report ===\n";
    for (const auto& vehicle : fleet) {
        vehicle->printSummary(); // correct fuelCost() called automatically
    }

    return 0;
}
▶ Output
=== Fleet Cost Report ===
Ford F-150 | Fuel: Petrol | Cost for 300 miles: $52.5
Tesla Model 3 | Fuel: Electric | Cost for 300 miles: $9.75
Honda Civic | Fuel: Petrol | Cost for 300 miles: $30
🔥
Interview Gold:Interviewers love asking 'what is the difference between function overriding and function overloading?' Overloading is compile-time: same name, different parameters, resolved by the compiler. Overriding is runtime: same name, same signature, resolved at runtime via the vtable. They look similar but are completely different mechanisms.

Multiple Inheritance and the Diamond Problem — When Families Get Complicated

C++ allows a class to inherit from more than one base class. This is powerful but introduces a notorious hazard: the Diamond Problem. It occurs when two base classes both inherit from the same grandparent. Without a fix, the grandparent's data gets duplicated into the final class — two copies of the same members, ambiguous access, and logic bugs.

The fix is virtual inheritance on the intermediate classes. Virtual inheritance tells the compiler: 'even if multiple paths lead to this base, instantiate it exactly once.' The virtual base is then constructed by the most-derived class directly, which is why the most-derived constructor must call the virtual base constructor explicitly.

A practical rule: reach for multiple inheritance only when the extra base classes are pure interfaces (abstract classes with only pure virtual functions and no data). Mixing data-carrying base classes via multiple inheritance quickly turns into a maintenance nightmare — favour composition instead.

DiamondProblem.cpp · CPP
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253
#include <iostream>
#include <string>

// Grandparent — the single shared root
class Device {
public:
    std::string deviceId;
    Device(const std::string& id) : deviceId(id) {
        std::cout << "Device constructed: " << id << "\n";
    }
    virtual ~Device() = default;
};

// virtual inheritance ensures only ONE Device sub-object exists
class NetworkDevice : virtual public Device {
public:
    NetworkDevice(const std::string& id) : Device(id) {}
    void sendPacket() { std::cout << deviceId << " sending packet\n"; }
};

class StorageDevice : virtual public Device {
public:
    StorageDevice(const std::string& id) : Device(id) {}
    void writeData() { std::cout << deviceId << " writing data\n"; }
};

// SmartRouter inherits from BOTH — classic diamond shape
// Because both parents used virtual inheritance, Device is constructed ONCE
// The most-derived class (SmartRouter) MUST call Device's constructor directly
class SmartRouter : public NetworkDevice, public StorageDevice {
public:
    SmartRouter(const std::string& id)
        : Device(id),            // required: most-derived calls virtual base
          NetworkDevice(id),
          StorageDevice(id) {}

    void status() {
        // No ambiguity — there's only one deviceId
        std::cout << "Router " << deviceId << " is operational\n";
    }
};

int main() {
    std::cout << "--- Without virtual inheritance, Device would construct TWICE ---\n";
    std::cout << "--- With virtual inheritance, it constructs exactly once:      ---\n\n";

    SmartRouter router("RTR-001");
    router.sendPacket();
    router.writeData();
    router.status();

    return 0;
}
▶ Output
--- Without virtual inheritance, Device would construct TWICE ---
--- With virtual inheritance, it constructs exactly once: ---

Device constructed: RTR-001
RTR-001 sending packet
RTR-001 writing data
Router RTR-001 is operational
⚠️
Watch Out:Virtual inheritance adds overhead — the compiler uses an extra pointer (vptr) to locate the single shared base. For hot paths in performance-critical code (game loops, audio callbacks), measure before committing to deep virtual inheritance hierarchies. If both bases are pure abstract interfaces with no data, there's no diamond to worry about at all.
AspectPublic InheritancePrivate Inheritance
Relationship modelledIS-A (Car is a Vehicle)IMPLEMENTED-IN-TERMS-OF (uses internals)
Base public members in derivedRemain publicBecome private
Base protected members in derivedRemain protectedBecome private
Accessible outside the derived classYes — full base API exposedNo — base API is hidden
Pointer/reference substitution (Liskov)Allowed — derived* converts to base*Not allowed — compiler error
Typical use casePolymorphic hierarchies, interfacesRarely used; prefer composition
Preferred alternative when overusedStore base as a private member instead

🎯 Key Takeaways

  • The inheritance access specifier (public/protected/private) controls how the base class's API is re-exposed through the derived class — public inheritance models IS-A; private inheritance models implemented-in-terms-of and is almost always better replaced by composition.
  • Always use the override keyword when overriding a virtual function — it turns a silent logic bug (mismatched signature creates a new function) into a compile-time error you can fix immediately.
  • Always declare a virtual destructor in any base class with virtual functions — skipping it causes derived destructors to be silently skipped when deleting through a base pointer, leaking resources with no warning.
  • The Diamond Problem is solved by virtual inheritance on the middle classes, but the most-derived class must then call the virtual base constructor directly — and virtual inheritance adds a vptr cost, so prefer pure-abstract interfaces to avoid the issue entirely.

⚠ Common Mistakes to Avoid

  • Mistake 1: Forgetting a virtual destructor in the base class — If you delete a derived object through a base class pointer and the destructor is not virtual, only the base destructor runs. The derived class's destructor is silently skipped, leaking any resources it owns. Fix: always declare virtual ~BaseClass() = default; in any class that has virtual functions or is designed to be inherited.
  • Mistake 2: Typo-ing the override function signature — Writing void draw(int x) in a derived class when the base declares void draw(int x) const creates a NEW function rather than overriding the base one. You get no error, but the virtual dispatch never reaches your new method. Fix: always add the override keyword — the compiler will immediately tell you 'this function does not override any base class method', revealing the mismatch.
  • Mistake 3: Calling a virtual function from a constructor or destructor — During construction, the vtable for the derived class hasn't been set up yet. Calling a virtual method from the base constructor always resolves to the BASE version, not the derived one — the exact opposite of what most people expect. Fix: never call virtual functions in constructors or destructors. Use a factory method or a separate init() method that's called after the object is fully constructed.

Interview Questions on This Topic

  • QWhat is the difference between virtual and non-virtual inheritance in C++, and when would you actually use virtual inheritance?
  • QWhy must you declare a destructor as virtual in a polymorphic base class? What exactly goes wrong if you don't, and can you describe the specific scenario?
  • QIf a derived class doesn't explicitly call the base class constructor in its initialiser list, what happens — and what are the rules around which base constructor is chosen?

Frequently Asked Questions

What is the difference between inheritance and composition in C++?

Inheritance models an IS-A relationship — a Car is a Vehicle — and lets the derived class substitute for the base. Composition models a HAS-A relationship — a Car has an Engine — by storing one class as a member of another. Prefer composition when you only need to reuse behaviour without exposing the parent's interface; prefer inheritance when you need polymorphic substitution.

Can a C++ class inherit from multiple base classes at the same time?

Yes. C++ supports multiple inheritance, unlike Java or C#. The syntax is class Derived : public BaseA, public BaseB {}. The main hazard is the Diamond Problem — when two bases share a common ancestor — which is resolved with virtual inheritance. In practice, multiple inheritance is safest when the extra bases are pure abstract interfaces with no data members.

What does it mean for a class to be abstract in C++, and how do you create one?

An abstract class in C++ is any class that contains at least one pure virtual function, declared with = 0 (e.g., virtual void draw() const = 0;). The compiler prevents you from creating an instance of an abstract class directly. Its purpose is to define an interface contract that all derived classes must fulfil. A derived class becomes concrete — and instantiable — only when it provides implementations for every inherited pure virtual function.

🔥
TheCodeForge Editorial Team Verified Author

Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.

← PreviousConstructors and Destructors in C++Next →Polymorphism in C++
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged