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
vptris 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. Thevtableis a static, per-class array of function pointers for all virtual functions. - Key Distinction: Every object gets its own
vptr, but there is only onevtableper class. AllDogobjects, for example, will have their ownvptr, but all these pointers will point to the sameDogvtable.
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:
- The
Basesubobject is still placed at the beginning of theDerivedobject. - Since
Baseis polymorphic, this subobject's first member is itsvptr. - The data members of the
Baseclass follow thevptr. - Finally, the data members of the
Derivedclass follow theBasesubobject.
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:
- The
Baseconstructor is called first. At this stage, the object is considered aBaseobject. Thevptris set to point to theBasevtable. - After the
Baseconstructor completes, theDerivedconstructor runs. - The
Derivedconstructor then overwrites thevptrto point to theDerivedvtable. The object is now fully aDerivedobject.
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;
};
- What is the most likely
sizeof(Vehicle)? - What is the most likely
sizeof(Car)? - Draw a simple memory map for an object of type
Car. Label thevptr,weight, andnum_doors, and indicate which parts form theVehiclesubobject.
Show answer
-
sizeof(Vehicle)is likely 16 bytes.- 8 bytes for the
vptr(because it has a virtual function). - 4 bytes for the
weightmember. - 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.
- 8 bytes for the
-
sizeof(Car)is likely 24 bytes.- It inherits the
vptr(8 bytes) andweight(4 bytes) fromVehicle. - 4 bytes for padding are inserted after
weightto ensure the next member is 8-byte aligned, maintaining the base subobject size of 16. - It adds its own
num_doorsmember (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.)
- It inherits the
-
Memory map for a
Carobject: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 bytesThe
Vehiclesubobject 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'svtable. - The layout is typically:
vptr, followed by base class data, followed by derived class data, with padding inserted by the compiler for alignment. - The
vptris 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.