C++ Inheritance Explained — Types, Pitfalls and Real-World Patterns
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.
#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; }
2023 Toyota Camry | Doors: 4
Make accessed directly: Toyota
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.
#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; }
Driving with 150 HP
Engine stopped
---
Engine started
Booster running at 150 HP
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.
#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; }
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
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.
#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; }
--- With virtual inheritance, it constructs exactly once: ---
Device constructed: RTR-001
RTR-001 sending packet
RTR-001 writing data
Router RTR-001 is operational
| Aspect | Public Inheritance | Private Inheritance |
|---|---|---|
| Relationship modelled | IS-A (Car is a Vehicle) | IMPLEMENTED-IN-TERMS-OF (uses internals) |
| Base public members in derived | Remain public | Become private |
| Base protected members in derived | Remain protected | Become private |
| Accessible outside the derived class | Yes — full base API exposed | No — base API is hidden |
| Pointer/reference substitution (Liskov) | Allowed — derived* converts to base* | Not allowed — compiler error |
| Typical use case | Polymorphic hierarchies, interfaces | Rarely used; prefer composition |
| Preferred alternative when overused | — | Store 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
overridekeyword 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 declaresvoid draw(int x) constcreates 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 theoverridekeyword — 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.
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.