Senior 4 min · March 06, 2026

C++ Inheritance — Missing Virtual Destructor Leak

Game server memory grew monotonically; deleting via base pointer leaked textures & audio.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Inheritance lets a derived class reuse and extend base class members.
  • Public inheritance models IS-A; private inheritance is an implementation detail.
  • Virtual functions enable runtime polymorphism; always mark overrides with override.
  • The diamond problem requires virtual inheritance; it adds vptr overhead.
  • A missing virtual destructor in a polymorphic base causes silent resource leaks.
  • Calling virtual functions from constructors/destructors resolves to the base version — not the derived one.
Plain-English First

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.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#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.
Production Insight
When a derived class constructor does not explicitly call a base constructor, the compiler invokes the base's default constructor. If the base has no default constructor, the code won't compile. This catches many teams off guard.
Always verify that base classes intended for inheritance have a sensible default constructor or document which parameterised constructor must be called.
Key Takeaway
Base class must be fully constructed before derived class logic runs.
Always initialise the base through the member initialiser list, never the constructor body.
If the base has no default constructor, the derived class must explicitly call the parameterised one.

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.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#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.
Production Insight
Mistaking private for public inheritance is a silent design error. Private inheritance exposes no interface, but still allows the derived class to access protected base members. This can lead to tightly coupled code that is hard to refactor.
When refactoring, check if : private Base can be replaced with composition. If the relationship is 'uses-internals', composition is almost always clearer.
Key Takeaway
Public inheritance = IS-A. Private inheritance = implemented-in-terms-of.
Default inheritance access is private for class and public for struct.
If you catch yourself using private inheritance, ask whether composition would be cleaner.

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.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#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.
Production Insight
Calling a virtual function from inside a constructor or destructor will NOT dispatch to the derived class version. The vtable during construction tracks the current class level only.
This causes subtle bugs: if a base constructor stores a callback that calls a virtual function, the callback later might point to the base version even after the object is fully constructed. Always document such patterns.
Key Takeaway
Always declare virtual functions with = 0 (pure virtual) or virtual in base and override in derived.
The compiler generates a vtable for each polymorphic class — one entry per virtual function.
Do not call virtual functions in constructors or destructors.

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.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#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.
Production Insight
Virtual inheritance adds a level of indirection (vptr) to every access of the virtual base's data members. In performance-critical code (e.g., game loop), this can cause cache misses and measurable slowdown. Profile before using virtual inheritance on a hot path.
Also, the most-derived class must explicitly initialise the virtual base, which adds constructor complexity and risks missing initialisations.
Key Takeaway
Virtual inheritance solves the diamond by constructing the shared base exactly once.
The most-derived class is responsible for calling the virtual base constructor.
Performance trade-off: extra vptr overhead. Prefer using pure abstract interfaces as intermediate bases to avoid diamond complications entirely.

Inheritance vs Composition — The Real Trade-off

One of the most common design debates is whether to model a relationship with inheritance or composition. The rule of thumb: inheritance is for polymorphic substitution (you need to treat objects through a common interface), composition is for code reuse (you just need the functionality). In production, composition almost always wins for reuse because it's less coupled, easier to test, and doesn't force you into a class hierarchy. However, inheritance is indispensable when you need to build plugin systems, framework callbacks, or any code that must operate on unknown types.

Here's a concrete example: instead of inheriting from a Logger to reuse logging behaviour, store a Logger member and delegate to it. This lets you swap loggers at runtime (file, network, mock) without changing the class hierarchy.

CompositionVsInheritance.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <iostream>
#include <memory>
#include <string>

// ---- Inheritance approach ----
class Logger {
public:
    virtual void log(const std::string& msg) const {
        std::cout << "[Inheritance] " << msg << "\n";
    }
    virtual ~Logger() = default;
};

class ServiceWithInheritance : public Logger {
public:
    void doWork() {
        // 'log' is inherited, but the relationship is now IS-A Logger
        log("Working...");
    }
};

// ---- Composition approach ----
class ServiceWithComposition {
    std::shared_ptr<Logger> logger_; // can be any Logger implementation
public:
    explicit ServiceWithComposition(std::shared_ptr<Logger> logger)
        : logger_(std::move(logger)) {}
    void doWork() {
        // delegation: uses logger, not inherits
        logger_->log("Working...");
    }
};

class FileLogger : public Logger {
public:
    void log(const std::string& msg) const override {
        std::cout << "[File] " << msg << "\n";  // pretend file I/O
    }
};

int main() {
    // Inheritance: fixes the Logger type at compile time
    ServiceWithInheritance svc_inherit;
    svc_inherit.doWork();

    // Composition: can swap logger at runtime
    auto fileLogger = std::make_shared<FileLogger>();
    ServiceWithComposition svc_compose(fileLogger);
    svc_compose.doWork();

    // Composition also works with the default Logger
    auto defaultLogger = std::make_shared<Logger>();
    ServiceWithComposition svc_compose2(defaultLogger);
    svc_compose2.doWork();

    return 0;
}
Output
[Inheritance] Working...
[File] Working...
[Inheritance] Working...
Mental Model: Inheritance is a contract, composition is a tool
  • IS-A relationships need public inheritance (e.g., Car IS-A Vehicle).
  • HAS-A or USES-A relationships need composition (e.g., Service HAS-A Logger).
  • Deep inheritance hierarchies (>3 levels) are brittle and hard to debug.
  • Composition allows runtime polymorphism via interfaces without coupling the class hierarchy.
  • If you find yourself writing 'protected' members to share implementation, consider composition instead.
Production Insight
Deep inheritance hierarchies (5+ levels) become impossible to trace during debugging. I've spent days chasing a bug that turned out to be a virtual function call resolving to a base 4 levels up.
Prefer shallow hierarchies (2-3 levels max) with composition for behavior reuse. The Liskov substitution principle is your litmus test: if a derived class cannot truly replace its base without changing the program's correctness, composition is the answer.
Key Takeaway
Favor composition over inheritance for code reuse.
Use inheritance only when you need polymorphic substitution (Liskov substitution).
Deep hierarchies are a maintenance trap — keep inheritance trees shallow.
When to Use Inheritance vs Composition
IfNeed polymorphic substitution (pass derived as base)?
UseUse public inheritance.
IfNeed to reuse implementation without exposing interface?
UseUse composition or private inheritance (prefer composition).
IfNeed to reuse interface only (no implementation)?
UseUse inheritance from a pure abstract base (interface).
IfMultiple child classes share only behavior, not identity?
UseUse composition: store a shared member or inject via constructor.
● Production incidentPOST-MORTEMseverity: high

Missing Virtual Destructor Causes Silent Resource Leak in Game Engine

Symptom
Game server memory usage grew monotonically. After player disconnections, memory was not reclaimed. The leak only occurred when deleting polymorphic objects via base class pointers.
Assumption
The team assumed that C++ automatically calls the most-derived destructor, just like it calls the most-derived virtual function.
Root cause
Base class GameObject had virtual functions but its destructor was non-virtual. Deleting through GameObject* called only ~GameObject(), skipping the derived class destructor that freed OpenGL textures and audio buffers.
Fix
Declared virtual ~GameObject() = default; in the base class.
Key lesson
  • Always add a virtual destructor if a class has at least one virtual function.
  • The destructor is the one function that must be virtual in polymorphic bases.
  • Tooling: Valgrind and ASan can catch such leaks during development.
Production debug guideQuick symptom-to-root-cause mapping for common inheritance bugs5 entries
Symptom · 01
Derived class function is never called (base version runs instead)
Fix
Check function signature match — missing const, different parameter types, or missing override keyword.
Symptom · 02
Memory leak when deleting via base pointer
Fix
Verify base class destructor is virtual. Use valgrind or -fsanitize=address to confirm which destructors run.
Symptom · 03
Compile error: cannot instantiate abstract class
Fix
Identify all pure virtual functions in the class hierarchy. Ensure derived class provides implementations for each.
Symptom · 04
Ambiguous member access (error: request for member is ambiguous)
Fix
Check for diamond inheritance. Add virtual inheritance on middle classes. Qualify member with base class name.
Symptom · 05
Object slicing: derived data lost when assigning to base by value
Fix
Avoid passing objects by value in polymorphic contexts. Use pointers/references and prevent slicing.
★ C++ Inheritance Debugging Cheat SheetQuick commands and checks for diagnosing inheritance problems in production code.
Virtual function not called correctly
Immediate action
Add `override` keyword and recompile to catch mismatch.
Commands
g++ -std=c++20 -Wall -Wextra -Wpedantic -Wnon-virtual-dtor
nm -C binary | grep vtable
Fix now
Ensure function signatures match exactly (include const). Use final to prevent unexpected overrides.
Memory leak in polymorphic class hierarchy+
Immediate action
Check if base destructor is virtual.
Commands
grep -r 'virtual.*~' include/ | grep -v '= default'
valgrind --leak-check=full ./app
Fix now
Add virtual ~Base() = default; to the base class.
Compiler error: ambiguous base class member+
Immediate action
Identify the diamond shape in your hierarchy.
Commands
g++ -E -P source.cpp | grep 'class.*:'
pahole binary | grep -A5 'class'
Fix now
Add virtual to the inheritance of the middle classes from the common base.
Object slicing (extra derived data lost)+
Immediate action
Check if you're passing objects by value.
Commands
grep -rn 'void.*(' src/ | grep -v '*'
clang-tidy --checks=readability-slicing
Fix now
Change function parameter to accept a pointer or reference.
Public vs Private Inheritance
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 baseNot allowed — compiler error
Typical use casePolymorphic hierarchies, interfacesRarely used; prefer composition
Preferred alternative when overusedStore base as a private member instead

Key takeaways

1
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.
2
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.
3
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.
4
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.
5
Favor composition over inheritance for code reuse
deep hierarchies are hard to debug and maintain.

Common mistakes to avoid

4 patterns
×

Forgetting a virtual destructor in the base class

Symptom
When deleting a derived object through a base pointer, only the base destructor executes. Derived class resources (memory, file handles, network connections) are never freed, causing silent resource leaks.
Fix
Declare virtual ~BaseClass() = default; in any base class that has virtual functions or is intended for polymorphic deletion.
×

Accidentally creating a new function instead of overriding because of a mismatched signature

Symptom
The derived function never runs when called polymorphically. Code compiles without warning if override is missing, and the base version is silently invoked.
Fix
Always add the override keyword after the function declaration. The compiler will produce an error if the signature does not exactly match a base class virtual function.
×

Calling a virtual function from a constructor or destructor

Symptom
The function call resolves to the current class version, not the most-derived version. This often causes unexpected behavior or crashes if the derived class expects its own implementation to run during construction.
Fix
Never call virtual functions in constructors or destructors. Use a separate init() method called after full construction, or restructure to avoid the need.
×

Object slicing by passing polymorphic objects by value

Symptom
The derived part of the object is sliced off when passed by value to a function expecting a base class. Only the base portion remains; virtual dispatch still works via the vptr, but derived data members are lost.
Fix
Always pass polymorphic objects by pointer or reference. Use const Base& for read-only access. If you must copy, implement a virtual clone() method.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
What is the difference between virtual and non-virtual inheritance in C+...
Q02SENIOR
Why must you declare a destructor as virtual in a polymorphic base class...
Q03SENIOR
If a derived class doesn't explicitly call the base class constructor in...
Q04JUNIOR
What is the difference between function overloading and function overrid...
Q05SENIOR
Explain the diamond problem and how virtual inheritance solves it. What ...
Q01 of 05SENIOR

What is the difference between virtual and non-virtual inheritance in C++, and when would you actually use virtual inheritance?

ANSWER
Virtual inheritance ensures that a base class sub-object is shared (only one instance) when it appears multiple times in a class hierarchy (e.g., classic diamond problem). Non-virtual inheritance creates separate copies of the base class for each derived branch. Use virtual inheritance when you have diamond-shaped multiple inheritance and want to avoid ambiguous duplicate members. However, it adds vptr overhead; often better to redesign to use pure abstract interfaces (no data) on the intermediate bases.
FAQ · 3 QUESTIONS

Frequently Asked Questions

01
What is the difference between inheritance and composition in C++?
02
Can a C++ class inherit from multiple base classes at the same time?
03
What does it mean for a class to be abstract in C++, and how do you create one?
🔥

That's C++ Basics. Mark it forged?

4 min read · try the examples if you haven't

Previous
Constructors and Destructors in C++
5 / 19 · C++ Basics
Next
Polymorphism in C++