C++ Inheritance — Missing Virtual Destructor Leak
Game server memory grew monotonically; deleting via base pointer leaked textures & audio.
20+ years shipping performance-critical C and C++ systems. Notes here come from systems that actually shipped.
- 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.
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.
Why Inheritance Without a Virtual Destructor Leaks
Inheritance in C++ lets a derived class reuse and extend the interface and implementation of a base class. The core mechanic is that a derived object contains a base class subobject, and access control, virtual functions, and destructor dispatch are all governed by the base class design. The critical detail: if you delete a derived object through a base pointer, the destructor must be virtual to invoke the derived destructor; otherwise, only the base destructor runs, leaking derived resources.
When you declare a base class with a non-virtual destructor and derive from it, the compiler generates a static call to the base destructor for any delete on a base pointer. This is correct only if the dynamic type matches the static type. In practice, polymorphic hierarchies (e.g., Shape* s = new Circle(...)) rely on virtual dispatch to call ~. Without Circle()virtual ~, the Shape()Circle destructor never executes, leaking heap memory, file handles, or other resources held by the derived part.
Use virtual destructors in any base class that is intended for polymorphic deletion — that is, any class with at least one virtual function. If a class is not meant to be deleted polymorphically (e.g., a mixin or a sealed hierarchy), mark it final or document the constraint. In real systems, this rule prevents silent resource leaks that are invisible in valgrind until the derived class adds a std::vector or a raw pointer.
Transaction base with virtual process() but a non-virtual destructor. When a RefundTransaction added a std::thread member, deleting through Transaction* leaked the thread handle, causing gradual descriptor exhaustion and process OOM after 10k refunds.final to make the constraint explicit and enforceable.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.
: 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.All Five Types of Inheritance in C++
C++ supports five forms of inheritance: single, multiple, multilevel, hierarchical, and hybrid. Understanding which one fits your problem is critical to keeping your codebase maintainable.
| Type | Description | Example |
|---|---|---|
| Single | One child inherits from exactly one parent. | class Car : public Vehicle |
| Multiple | One child inherits from two or more direct parents. | class SmartDevice : public Camera, public WiFi |
| Multilevel | A chain: A → B → C, where C inherits B, B inherits A. | class Mammal : public Animal; class Dog : public Mammal |
| Hierarchical | One parent is inherited by multiple children. | class Vehicle { … }; class Car : public Vehicle; class Bike : public Vehicle |
| Hybrid | Combination of multiple inheritance forms, often with a diamond. | class A; class B : virtual A; class C : virtual A; class D : B, C |
The diagram below shows how these types relate:
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.
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.: private Base can be replaced with composition. If the relationship is 'uses-internals', composition is almost always clearer.class and public for struct.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.
= 0 (pure virtual) or virtual in base and override in derived.VTable and VPointer: Memory Layout of Virtual Functions
When a class has at least one virtual function, the compiler generates a static array called the virtual table (vtable). Each virtual function has a function pointer slot in this table. Every object of that class also gets a hidden vpointer (vptr) that points to its class's vtable.
Here's the memory layout: on the left is the object memory (stack/heap), which contains data members plus the vptr. The vptr points to the vtable, which stores pointers to the actual function implementations. When you call a virtual function through a base pointer, the generated assembly reads the vptr from the object, indexes into the vtable, and jumps to the correct derived implementation.
The diagram below shows the layout for a Derived object when Base has a virtual function that foo()Derived overrides:
std::variant or manual type dispatching to avoid vtable overhead.Rules for Virtual Functions in C++
Follow these rules to avoid common pitfalls when using virtual functions:
| Rule | Explanation |
|---|---|
| A virtual function must be a non-static member of a class. | Free functions or static members cannot be virtual. |
The base class function must be declared virtual. | If not, the derived function hides instead of overrides. |
| The derived override must have an exactly matching signature. | Different parameters or const-qualification create a new function, not an override. Always use override. |
Virtual functions can be final in derived classes. | Prevents further overriding. Useful for security or performance (devirtualisation). |
| Constructors cannot be virtual. | That would require an already-constructed vtable to dispatch. |
| Destructors can (and should) be virtual in polymorphic bases. | Ensures correct cleanup through base pointers. |
Pure virtual functions (= 0) make the class abstract. | The class cannot be instantiated; any derived class must implement all pure virtual functions to become concrete. |
| Virtual functions can have default arguments. | But the default argument is determined by the static type, not the dynamic type. Avoid default arguments in virtual functions. |
| Calling a virtual function in a constructor/destructor uses the base version. | The vtable is not fully formed until the derived constructor completes. |
override on every derived virtual function. Avoid default arguments in virtual functions. Never call virtual functions from constructors/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.
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.
- 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.
Advantages and Disadvantages of Inheritance in C++
Inheritance is a powerful tool, but like any tool it has trade-offs. Use this table to decide when to lean in and when to pull back.
| Advantages | Disadvantages |
|---|---|
| Code reuse: Base class logic is automatically available to all derived classes. | Tight coupling: Derived classes are tightly coupled to base class internals. Changes in the base can break all derived classes. |
| Polymorphism: Enables runtime dispatch through virtual functions, making plugins and flexible designs possible. | Fragile base class problem: Adding a new virtual function to a base can cause unexpected behaviour in derived classes. |
| Logical hierarchy: Models real-world IS-A relationships clearly. | Deep hierarchies are hard to maintain: 5+ levels of inheritance make debugging and understanding code difficult. |
| Extensibility: New derived classes can add functionality without modifying existing code (open/closed principle). | Multiple inheritance complexity: Diamond problem and virtual inheritance add hidden overhead and mental burden. |
| Interface contracts: Abstract bases force derived classes to implement required methods. | Object slicing: Passing by value loses derived data; requires careful use of pointers/references. |
| Standard library integration: Many STL containers and algorithms work seamlessly with polymorphic objects. | Performance overhead: Virtual dispatch adds indirection (vptr, vtable lookup) that prevents inlining and can cause cache misses. |
In production, use inheritance sparingly. When you do use it, keep the hierarchy shallow, mark overrides with override, and always consider whether composition would serve better.
Practice Problems: C++ Inheritance
Apply what you've learned with these hands-on problems. Focus on understanding the concepts rather than memorising syntax.
Problem 1: Virtual Destructor Detection Given the following code, will there be a memory leak? If so, why? ``cpp class Base { public: ~`Base() {} }; class Derived : public Base { int data = new int[100]; }; int main() { Base p = new Derived(); delete p; } Answer: Yes – leak. Base destructor is non-virtual, so ~Derived() never runs. Fix: add virtual ~Base() = default;`.
Problem 2: Multiple Inheritance Ambiguity ``cpp struct A { void `foo() {} }; struct B : A {}; struct C : A {}; struct D : B, C {}; int main() { D d; d.foo(); } Does this compile? If not, what's the fix? Answer: No – ambiguous call to A::foo(). Fix: use virtual inheritance (class B : virtual public A, class C : virtual public A`).
Problem 3: Override vs Overload ``cpp class Base { public: virtual void print(int x) { std::cout << "Base"; } }; class Derived : public Base { public: void print(double x) { std::cout << "Derived"; } }; ` When calling Derived d; d.print(5), which version runs? Answer: Derived version because int converts to double. The function print(double) is a new function (overload), not an override. Add override` to catch this mistake. If you want to override, match signature exactly.
Problem 4: Diamond Problem without Virtual Inheritance Given the diamond shape with a data member int value in the root, how many copies of value exist in the most-derived class? Answer: Two copies – one from each path. Fix: use virtual inheritance on intermediate classes.
Problem 5: Virtual Table Construction How many vtable entries does a class with three virtual functions and no overrides have? Answer: Three entries (one per virtual function). Each derived class that overrides any of them gets its own vtable with updated pointers.
-Wnon-virtual-dtor) and static analysis tools to automate detection.Slicing: The Silent Data Loss That Kills Polymorphism
You pass an object by value to a function expecting a base class. Compiles fine. But the derived part? Gone. Sliced off. This is C++'s version of a silent type cast that discards everything you just inherited.
The root cause is how C++ handles value semantics. When you copy a Derived object into a Base parameter, the compiler only copies the Base portion. The compiler doesn't know — and doesn't care — about the Derived's virtual table pointer or extra members. It just memcpy's the base part. Polymorphism? Dead. Destructor? Base's version runs. You just lost any extended state and behavior.
Senior teams enforce a rule: never pass polymorphic objects by value. Always use pointers or references. If someone checks in code with void process(SensorData data), you flag it. Because that function will happily slice every derived sensor type you throw at it, and nobody will notice until the temperature readings stop making sense.
Virtual Inheritance: Why Your Diamond Won't Kill You If You Do It Right
Everyone fears the diamond problem. Two base classes, both deriving from the same grandparent, a single derived class inheriting both. Without virtual inheritance, you get two copies of the grandparent. Ambiguity, state duplication, and headaches at the water cooler.
The fix: virtual keyword on the inheritance specifier. But here's what the tutorials won't tell you — virtual inheritance changes the memory layout and construction order drastically. The most derived class becomes responsible for constructing the virtual base. Not the intermediate classes. So if a base class in the middle tries to initialize the virtual base, it gets silently ignored. This has cost teams days of debugging.
Second pain: casting. You can't static_cast from a virtual base to a derived class. The compiler doesn't know where the derived class sits relative to the virtual base at compile time. You need dynamic_cast or a C-style cast (don't). If you have virtual inheritance, expect RTTI overhead and slightly slower cast operations.
Third: construction order. Virtual bases are always constructed first, in depth-first left-to-right order of the inheritance graph. If your virtual base constructor throws, the entire object hierarchy collapses. Protect that constructor like it's hosting the king.
Missing Virtual Destructor Causes Silent Resource Leak in Game Engine
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.virtual ~GameObject() = default; in the base class.- 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.
override keyword.valgrind or -fsanitize=address to confirm which destructors run.g++ -std=c++20 -Wall -Wextra -Wpedantic -Wnon-virtual-dtornm -C binary | grep vtablefinal to prevent unexpected overrides.Key takeaways
override keyword when overriding a virtual functionCommon mistakes to avoid
4 patternsForgetting a virtual destructor in the base class
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
override is missing, and the base version is silently invoked.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
init() method called after full construction, or restructure to avoid the need.Object slicing by passing polymorphic objects by value
const Base& for read-only access. If you must copy, implement a virtual clone() method.Interview Questions on This Topic
What is the difference between virtual and non-virtual inheritance in C++, and when would you actually use virtual inheritance?
Frequently Asked Questions
20+ years shipping performance-critical C and C++ systems. Notes here come from systems that actually shipped.
That's C++ Basics. Mark it forged?
13 min read · try the examples if you haven't