T.TAO
Back to Blog
/5 min read/Programming

C++ #6 Inheritance

#C++#Programming#ComputerSystems

This note covers C++ inheritance and what actually happens in memory when you use it.

Inheritance is embedding

C++class Base {
    int a;
};

class Derived : public Base {
    int b;
};

The layout of Derived is: a complete Base subobject first, then Derived's own members.

Plain TextDerived object
+---------------------+
| Base::a    (4 bytes)|   <- Base subobject
+---------------------+
| Derived::b (4 bytes)|
+---------------------+

That explains two things. First, a Derived* converts implicitly to a Base* β€” the Base subobject sits at the start of the object, so the pointer value does not even have to change. Second, assigning a Derived object by value to a Base variable causes slicing: only the Base part is copied and b is quietly dropped.

C++Derived d;
Base b = d;    // sliced: the Derived part of d is gone
Base& r = d;   // fine: a reference does not copy

Polymorphism always goes through a pointer or a reference precisely because passing by value slices.

The three inheritance accesses

C++class D1 : public Base    { };   // public inheritance
class D2 : protected Base { };   // protected inheritance
class D3 : private Base   { };   // private inheritance

They set an upper bound on the visibility of base members inside the derived class:

Base memberpublic inheritanceprotected inheritanceprivate inheritance
publicpublicprotectedprivate
protectedprotectedprotectedprivate
privateinaccessibleinaccessibleinaccessible

public inheritance expresses an is-a relationship: anywhere a Base is accepted, a Derived should be accepted too β€” the Liskov substitution principle.

private inheritance expresses implemented-in-terms-of: you want to reuse the base's implementation without exposing the relationship. Most of the time composition (a member variable) is clearer than private inheritance. It earns its place only when you need to override a virtual function of the base, or when you want the empty base optimization.

protected inheritance is almost never useful in practice.

The constructor and destructor chain

Construction starts at the most-base class; destruction runs in reverse:

C++struct A { A() { puts("A()"); } ~A() { puts("~A()"); } };
struct B : A { B() { puts("B()"); } ~B() { puts("~B()"); } };

B b;
// prints: A()  B()  ~B()  ~A()

A derived constructor calls the base constructor before running its own body. Without an explicit call it invokes the base's default constructor; to pass arguments you must use the initializer list:

C++class Derived : public Base {
public:
    Derived(int x) : Base(x), y_(x * 2) {}
private:
    int y_;
};

Do not call virtual functions from a constructor

C++struct Base {
    Base() { init(); }                    // dangerous
    virtual void init() { puts("Base"); }
};
struct Derived : Base {
    void init() override { puts("Derived"); }
};

Derived d;   // prints "Base", not "Derived"

While Base's constructor runs, the Derived part does not exist yet, and the vtable pointer still points at Base's vtable. The language is right to define it this way β€” the alternative would call a function that reads uninitialized members β€” but it is very often not what the author expected. The same applies in destructors.

Base destructors must be virtual

C++Base* p = new Derived();
delete p;   // if ~Base is not virtual, ~Derived() never runs

That is undefined behaviour, and in practice it leaks whatever resources Derived owned. The rule is simple: if a class might be deleted through a base pointer, its destructor must be virtual. Conversely, if a class is not meant to be inherited from, mark it final β€” it states the intent and lets the compiler devirtualize.

Name hiding

A function in a derived class hides every overload of that name in the base, even ones with different parameters:

C++struct Base {
    void f(int);
    void f(double);
};
struct Derived : Base {
    void f(const char*);   // hides both Base::f overloads
};

Derived d;
d.f(42);   // compile error: 42 does not convert to const char*

This is not an overload-resolution problem. Name lookup stops as soon as it finds Derived::f; it never looks at Base at all. To pull the base overloads back in:

C++struct Derived : Base {
    using Base::f;
    void f(const char*);
};

Multiple inheritance and the diamond

C++ lets a class inherit from several bases, which brings the diamond problem:

C++struct Animal { int age; };
struct Bird : Animal {};
struct Fish : Animal {};
struct FlyingFish : Bird, Fish {};   // contains two Animals

A FlyingFish object holds two independent Animal subobjects, so ff.age is an ambiguity error and you have to write ff.Bird::age.

The answer is virtual inheritance:

C++struct Bird : virtual Animal {};
struct Fish : virtual Animal {};
struct FlyingFish : Bird, Fish {};   // one Animal

Virtual inheritance is not free: the object needs an extra virtual-base pointer to locate the shared subobject, member access costs one more indirection, and the most-derived class becomes responsible for constructing the virtual base β€” an Animal(...) initializer written in an intermediate class is simply ignored, which surprises people regularly.

The practical advice: restrict multiple inheritance to the shape one implementation base plus several pure interfaces. A pure interface β€” only pure virtual functions, no data members β€” cannot produce a diamond worth worrying about, so virtual inheritance never comes up. This is also why Java and C# allow single inheritance plus multiple interfaces and nothing more.

The runtime half of inheritance β€” virtual functions and the vtable β€” is covered in C++ #8 Polymorphism.

  1. 01C++ #1 Data and Memory
  2. 02C++ #2 Struct and Union
  3. 03C++ #3 Pointers and Arrays
  4. 04C++ #4 Functions
  5. 05C++ #5 Objects and Classes
  6. 06C++ #6 Inheritance
  7. 07C++ #7 Copy Control and Operator Overloading
  8. 08C++ #8 Polymorphism
  9. 09C++ #9 C++11 New Features
  10. 10C++ #10 C++14 New Features
  11. 11C++ #11 C++17 New Features