Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 16

Derived Class Memory Layout: Single Inheritance

Recommended resources

Hello and welcome back!

In our previous lesson, we concluded our exploration of class fundamentals by examining access specifiers. We established that public, protected, and private are crucial for good API design and are enforced strictly at compile-time, with no impact on runtime performance or memory layout. This was a key step in understanding how C++ helps us write robust, encapsulated code.

Now, we move into the first lesson of our third module, "Runtime Polymorphism Internals." Today's goal is to explain the memory layout of derived classes with single inheritance, focusing on base class subobjects. While access specifiers control who can access members, inheritance dictates how members are physically arranged in memory. This is the foundation upon which C++ builds its powerful runtime polymorphism features.

For someone with your C background, this concept will feel familiar. You know how a struct can contain another struct as a member. C++ inheritance, at a memory level, often works in a very similar way.

1. The Base Class Subobject: An "Is-A" Relationship in Memory

Let's begin with the simplest case: single inheritance without any virtual functions. The "is-a" relationship (e.g., a Dog is an Animal) is physically represented by placing the Base class's data members directly inside the Derived object's memory layout. This embedded part is called the base class subobject.

Most compilers, for efficiency and simplicity, will lay out the derived object's memory with the base class subobject first, followed by the derived class's own data members.

class Base {
public:
    int base_data;
};

class Derived : public Base {
public:
    int derived_data;
};

// Memory layout of a 'Derived' object:
// |-------------------|
// | base_data         |  <-- Base class subobject
// |-------------------|
// | derived_data      |
// |-------------------|

This layout has a critical consequence: the memory address of a Derived object is the same as the memory address of its Base subobject. This allows a Derived* to be converted to a Base* without any calculation—the pointer value remains the same. This is a fundamental optimization that makes polymorphism efficient.

Dive into C++ Object Memory Layout with Examples

The article 'Dive into C++ Object Memory Layout with Examples' provides an excellent explanation of this with a clear GDB-based visualization. It confirms that the base class members appear first in the memory layout.

Please read the section titled 'Inheritance Without Virtual Functions'. Focus on the GDB output and the accompanying diagram showing how the Derived object contains the Basic class's members first, followed by its own.


2. Adding Polymorphism: The Arrival of the vptr

The memory layout we just saw is for non-polymorphic classes. The moment we introduce at least one virtual function into the base class, the compiler must add a mechanism for runtime dispatch. This mechanism introduces a new, hidden member to our objects: the virtual pointer, or vptr.

  • What is it? The vptr is a hidden pointer, added by the compiler to every object of a polymorphic class.
  • What does it do? It points to the virtual table (vtable) for that object's class. The vtable is a static, per-class array of function pointers for all virtual functions.
  • Key Distinction: Every object gets its own vptr, but there is only one vtable per class. All Dog objects, for example, will have their own vptr, but all these pointers will point to the same Dog vtable.

Adding this vptr increases the size of the object.

C++ Inheritance Explained: V-Tables, Virtual Destructors & Edge Cases

The video 'C++ Inheritance Explained' from DeepDiveDev gives one of the clearest introductions to this topic. It demonstrates the size increase and explains the vptr and vtable concepts from first principles.

Please watch from 03:00 to 07:02. This section covers: How adding a virtual function increases the object's size (03:00 - 04:30). The discovery of the hidden vptr at the beginning of the object's memory (04:30 - 05:44). The crucial distinction between the per-object vptr and the per-class vtable (05:44 - 07:02).

The video brilliantly shows that the vptr is typically placed at the very beginning of the object's memory, at offset 0.


3. The Complete Picture: Layout of a Polymorphic Derived Class

Now let's combine these two ideas: the base class subobject and the vptr.

When a Derived class inherits from a polymorphic Base class:

  1. The Base subobject is still placed at the beginning of the Derived object.
  2. Since Base is polymorphic, this subobject's first member is its vptr.
  3. The data members of the Base class follow the vptr.
  4. Finally, the data members of the Derived class follow the Base subobject.

The memory layout of a polymorphic Derived object looks like this:

class Base {
public:
    virtual void func() {}
    int base_data;
};

class Derived : public Base {
public:
    void func() override {}
    int derived_data;
};

// Memory layout of a 'Derived' object:
// |-------------------|  \
// | vptr              |   |
// |-------------------|   |  Base class subobject
// | base_data         |   |
// |-------------------|  /
// | (padding, if any) |
// |-------------------|
// | derived_data      |
// |-------------------|
// | (padding, if any) |
// |-------------------|

Note that the vptr inside a Derived object will point to the vtable for the Derived class, not the Base class. Let's see a practical visualization of this.

Visualizing the C++ Object Memory Layout Part 1: Single Inheritance

The article 'Visualizing the C++ Object Memory Layout' provides a fantastic, hands-on demonstration of this exact layout. It uses modern C++ features to inspect the raw memory of objects and prints an annotated breakdown.

Read from the start down to the end of the 'Padding' section. The 'Simple Single Inheritance' section sets up the Base and Derived classes. 'Memory Layout of Derived' introduces the concept of inspecting the layout. Focus on the code output under 'Displaying Offset, Size and Raw Bytes'. The 'Derived obj bytes (annotated)' section is the most important part—it shows the vptr at offset 0, followed by b_data and then d_data. The 'Padding' section explains why the Base object might be larger than you expect, a concept that should be familiar from your systems programming knowledge.

The article confirms our model: the derived object is a contiguous block of memory starting with the base class subobject (which itself starts with the vptr), followed by the derived class's members.


4. A Note on Construction Order and the vptr

How does the vptr get its correct value? This happens during construction, and the order is critical. When you create a Derived object:

  1. The Base constructor is called first. At this stage, the object is considered a Base object. The vptr is set to point to the Base vtable.
  2. After the Base constructor completes, the Derived constructor runs.
  3. The Derived constructor then overwrites the vptr to point to the Derived vtable. The object is now fully a Derived object.

This step-by-step "promotion" is why calling a virtual function from within a base class constructor will always call the base class version of that function, never the derived one. At that moment in its lifetime, the object's dynamic type is the base type.

C++ Under the Hood: Internal Class Mechanisms - Chris Ryan - CppCon 2024

The CppCon talk 'C++ Under the Hood' walks through this construction process with diagrams, showing how the vtable pointer is updated as the 'onion' of inheritance is built from the inside out.

Please watch from 05:31 to 09:08. This segment provides a detailed walkthrough of object construction, explicitly showing the vptr being set for the base class first, and then being updated for the derived class.

Test your understanding!

Given the following classes on a 64-bit system (where pointers are 8 bytes and ints are 4 bytes):

class Vehicle {
public:
    virtual void start() {}
    int weight;
};

class Car : public Vehicle {
public:
    void start() override {}
    int num_doors;
};
  1. What is the most likely sizeof(Vehicle)?
  2. What is the most likely sizeof(Car)?
  3. Draw a simple memory map for an object of type Car. Label the vptr, weight, and num_doors, and indicate which parts form the Vehicle subobject.
Show answer
  1. sizeof(Vehicle) is likely 16 bytes.

    • 8 bytes for the vptr (because it has a virtual function).
    • 4 bytes for the weight member.
    • 4 bytes of padding to align the total size to a multiple of the largest member's alignment (the 8-byte pointer).
    • Total: 8 + 4 + 4 = 16.
  2. sizeof(Car) is likely 24 bytes.

    • It inherits the vptr (8 bytes) and weight (4 bytes) from Vehicle.
    • 4 bytes for padding are inserted after weight to ensure the next member is 8-byte aligned, maintaining the base subobject size of 16.
    • It adds its own num_doors member (4 bytes).
    • 4 bytes of final padding to make the total size a multiple of 8.
    • Total: (8 for vptr + 4 for weight + 4 for padding) + (4 for num_doors + 4 for padding) = 24.
      (Note: The exact layout can vary, but this is a very common implementation.)
  3. Memory map for a Car object:

    Offset | Member         | Description
    -------|----------------|----------------------------------
    +0     | vptr           | (8 bytes) Points to Car's vtable
    +8     | weight         | (4 bytes)
    +12    | padding        | (4 bytes)
    -------|----------------|---------------------------------- <-- End of Vehicle subobject
    +16    | num_doors      | (4 bytes)
    +20    | padding        | (4 bytes)
    -------|----------------|----------------------------------
    Total size: 24 bytes
    

    The Vehicle subobject occupies the first 16 bytes.


Conclusion

Today, we peeled back the abstraction of inheritance to see its physical representation in memory. This is the bedrock upon which the entire C++ object-oriented system is built.

Key Takeaways:

  • Single inheritance is implemented by embedding a base class subobject within the derived class object's memory.
  • In a non-polymorphic class, this subobject contains only the base class's data members.
  • In a polymorphic class, the first element of the subobject is the hidden vptr, which points to the class's vtable.
  • The layout is typically: vptr, followed by base class data, followed by derived class data, with padding inserted by the compiler for alignment.
  • The vptr is set during construction, starting with the base class vtable and being overwritten by each successive derived class constructor.

We now understand the structure of a polymorphic object. In our next lesson, we will analyze that structure in action. We will trace a virtual function call through the vptr and vtable to see exactly how dynamic dispatch works at runtime and analyze its overhead compared to a regular function call.