The Storage Model of C++ Classes

The Realm of Memory Management

Hello there,

Welcome to the intriguing world of C++ memory management! Understanding how classes and objects fit together in memory is essential for writing efficient code. With features like inheritance, polymorphism, and abstract base classes, C++ offers a complex but rewarding landscape for memory layouts.

In this article, we’ll explore various scenarios that illustrate how data members, virtual functions, and inheritance interact. Whether you’re just starting out or looking to sharpen your skills, you'll discover valuable insights that will enhance your programming journey. Let’s dive in and unravel the mysteries of memory layouts in C++!

Memory Layout of Simple & Non-Polymorphic Object in C++

Consider the following C++ class X:

class X {
    int     x;     // 4 bytes
    float   xx;    // 4 bytes

public:
    X() {}         // Constructor
    ~X() {}        // Destructor

    void printInt() {}   // Member function
    void printFloat() {} // Member function
};

This is a simple, non-polymorphic C++ class containing two data members (int and float), a constructor, a destructor, and two non-virtual member functions. Let's now break down the memory layout of this class.

Memory Layout

  1. Data Members:

    • int x → 4 bytes.

    • float xx → 4 bytes.

    • These data members are stored in the stack segment when an object of class X is created on the stack.

  2. Member Functions:

    • The member functions X(), ~X(), printInt(), and printFloat() are part of the text segment (code segment) of the program. They do not occupy space in the object itself, as they are stored elsewhere in memory.

Memory Layout of Class X:

      |                        |          
      |------------------------| <------ X class object memory layout
      |        int X::x        |       // Data member
      |------------------------|  stack segment
      |       float X::xx      |       |   
      |------------------------|       |
      |                        |      \|/
      |                        |    
      |                        |
------|------------------------|----------------
      |         X::X()         |    // Constructor
      |------------------------|       |   
      |        X::~X()         |    // Destructor
      |------------------------|      \|/
      |      X::printInt()     |  text segment
      |------------------------|    // Member function
      |     X::printFloat()    |    // Member function
      |------------------------|
      |                        |

Explanation:

  1. Data Members in the Stack Segment:

    • The int x and float xx are laid out sequentially in memory. Since the size of an int is typically 4 bytes and the size of a float is 4 bytes, the combined size of the object (without considering alignment) is 8 bytes. The memory for these data members is allocated on the stack when an object is instantiated on the stack.
  2. Text Segment:

    • Member functions such as the constructor (X()), the destructor (~X()), and other member functions like printInt() and printFloat() are stored in the text segment of the program (also known as the code segment).

    • Non-virtual member functions do not occupy space in the object itself. They are stored globally in the program’s text segment, and every instance of the class refers to the same code in the text segment. The function's address is not stored in each object.

  3. Constructor and Destructor:

    • The constructor X() and destructor ~X() are also part of the text segment. They are invoked when an object is created or destroyed but do not impact the size of the object in memory.

Memory Layout of Classes with Different Features

1. Memory Layout with Static Data Members

class Y {
    static int  staticVar;  // Static data member (shared)
    int         y;          // Non-static data member

public:
    Y() {}
    ~Y() {}
    void func() {}
};

Memory Layout:

      |                        |          
      |------------------------| <------ Y class object memory layout
      |        int Y::y        |       // Instance data member
      |------------------------|  stack segment
      |                        |   
      |                        |   
------|------------------------|----------------
      |         Y::Y()         |    // Constructor
      |------------------------|   
      |        Y::~Y()         |    // Destructor
      |------------------------|      text segment
      |       Y::func()        |    // Member function
      |------------------------|
      |                        |   
------|------------------------|----------------
      |   static int Y::staticVar   |  // Static data member, global segment
      |------------------------|

Explanation:

  • Static Members: staticVar is stored in a global/static segment of memory, separate from the object instance. It is shared across all instances of the class and does not contribute to the size of individual objects.

  • Instance Members: Only the int y takes space in the object instance in the stack.

2. Memory Layout with Virtual Functions (Polymorphism)

class Base {
public:
    virtual void virtualFunc() {}  // Virtual function
    int baseVar;                   // Data member
};

class Derived : public Base {
public:
    void virtualFunc() override {} // Override base virtual function
    int derivedVar;
};

Memory Layout:

      |                        |          
      |------------------------| <------ Derived class object memory layout
      |     int Base::baseVar   |  // Base class data member
      |------------------------|  stack segment
      |    int Derived::derivedVar |  // Derived class data member
      |------------------------|
      |                        |   
------|------------------------|----------------
      |         Base::vptr      |  // Virtual pointer (vptr) for virtual table
      |------------------------|   
      |         Base::virtualFunc()   |  // Virtual table (vtable) entry
      |------------------------|
      |         Derived::virtualFunc() |  // Virtual table (vtable) entry
      |------------------------|   
------|------------------------|----------------
      |         Base::Base()    |  // Base constructor in text segment
      |------------------------|    text segment
      |        Derived::Derived() |  // Derived constructor in text segment
      |------------------------|

Explanation:

  • Virtual Pointer (vptr): For every object of Derived, a hidden pointer (vptr) is added, which points to a virtual table (vtable) that stores function pointers for virtual functions.

  • Virtual Table (vtable): The virtual table contains pointers to the actual function implementations of virtualFunc(). The table is created in the text segment, and the vptr points to this table for each object.

  • Data Members: baseVar from the base class and derivedVar from the derived class are stored sequentially in memory, starting at the location of the derived class object.

3. Memory Layout with Multiple Inheritance

class Base1 {
public:
    int b1;   // Base1 data member
};

class Base2 {
public:
    int b2;   // Base2 data member
};

class DerivedMultiple : public Base1, public Base2 {
public:
    int derivedData;  // Derived class data member
};

Memory Layout:

      |                        |          
      |------------------------| <------ DerivedMultiple class object memory layout
      |     int Base1::b1       |  // Base1 data member
      |------------------------|  
      |     int Base2::b2       |  // Base2 data member
      |------------------------|  stack segment
      |  int DerivedMultiple::derivedData |  // Derived class data member
      |------------------------|   
      |                        |    
------|------------------------|----------------
      |      Base1::Base1()     |   // Base1 constructor
      |------------------------|    text segment
      |      Base2::Base2()     |   // Base2 constructor
      |------------------------|   
      |    DerivedMultiple::DerivedMultiple() |   // DerivedMultiple constructor
      |------------------------|

Explanation:

  • Data Members: In multiple inheritance, the data members from Base1 and Base2 are laid out sequentially before the data members of the derived class.

  • Constructors: The constructors for Base1, Base2, and DerivedMultiple are stored in the text segment. Each constructor is called when an object of DerivedMultiple is instantiated.

4. Memory Layout with Virtual Inheritance

class VirtualBase {
public:
    int vbData;  // Virtual base class data member
};

class Derived1 : virtual public VirtualBase {
public:
    int d1Data;
};

class Derived2 : virtual public VirtualBase {
public:
    int d2Data;
};

class FinalDerived : public Derived1, public Derived2 {
public:
    int finalData;
};

Memory Layout:

      |                        |          
      |------------------------| <------ FinalDerived class object memory layout
      |         vptr            |  // Virtual pointer for virtual inheritance
      |------------------------|  
      |   int VirtualBase::vbData |  // Shared virtual base class data member
      |------------------------|  
      |   int Derived1::d1Data  |  // Derived1 data member
      |------------------------|  
      |   int Derived2::d2Data  |  // Derived2 data member
      |------------------------|  
      |   int FinalDerived

::finalData |  // FinalDerived data member
      |------------------------|

Explanation:

  • Virtual Base Class: In virtual inheritance, the virtual base class (VirtualBase) is shared between Derived1 and Derived2. It is only stored once in memory, and a virtual pointer (vptr) is used to access it.

  • Data Members: The data members of Derived1, Derived2, and FinalDerived are stored in sequence after the virtual base class data.

Advanced Memory Layout Scenarios in C++ Classes

Let's now extend the discussion of memory layout to even more complex scenarios involving different C++ class features like multiple virtual inheritance, bit-fields, virtual destructors, and alignment. These examples will cover edge cases and advanced memory layout considerations.


Memory Layout with Virtual Destructor

A virtual destructor ensures that the correct destructor is called when an object is deleted via a base class pointer. This adds overhead to the class in terms of memory layout.

class Base {
public:
    virtual ~Base() {}  // Virtual destructor
    int baseData;       // Base class data member
};

class Derived : public Base {
public:
    int derivedData;    // Derived class data member
};

Memory Layout:

      |                        |          
      |------------------------| <------ Derived class object memory layout
      |       Base::vptr        |  // Virtual pointer for vtable (due to virtual destructor)
      |------------------------|  
      |     int Base::baseData  |  // Base class data member
      |------------------------|  
      |   int Derived::derivedData |  // Derived class data member
      |------------------------|    
------|------------------------|----------------
      |      Base::~Base()      |   // Virtual destructor in the vtable
      |------------------------|    text segment
      |   Derived::~Derived()   |   // Destructor in the vtable
      |------------------------|

Explanation:

  • Virtual Pointer (vptr): Since the destructor is virtual, the class requires a vptr that points to a vtable containing a pointer to the actual destructor to be called.

  • Memory Size: The addition of the virtual pointer increases the size of both the Base and Derived class objects. Without considering alignment, sizeof(Base) = 8 (int + vptr), and sizeof(Derived) = 12 (baseData + derivedData + vptr).


Memory Layout with Bit-fields

Bit-fields allow packing multiple members into a smaller space by specifying how many bits each member occupies. This can reduce memory usage but also complicates the layout.

class BitFieldExample {
    unsigned int field1 : 5;  // 5 bits
    unsigned int field2 : 3;  // 3 bits
    unsigned int field3 : 8;  // 8 bits
};

Memory Layout:

      |                        |          
      |------------------------| <------ BitFieldExample class object memory layout
      |  field1 (5 bits)        |       // Packed bit-fields
      |  field2 (3 bits)        |
      |------------------------|  Stored in the same 4-byte int
      |  field3 (8 bits)        |
      |------------------------|

Explanation:

  • Bit-fields Packing: field1 and field2 are packed into the first 4-byte word (int), while field3 is packed into another 4-byte word.

  • Memory Size: In this case, the total size of the object may still be sizeof(int) = 4 bytes depending on how the compiler optimizes the memory layout for bit-fields.


Memory Layout with Virtual Inheritance and Virtual Functions

This is a complex scenario combining both virtual inheritance and virtual functions, which requires handling both virtual pointers for function dispatch and managing shared virtual base classes.

class VirtualBase {
public:
    virtual void virtualFunc() {}  // Virtual function
    int vbData;
};

class Derived1 : virtual public VirtualBase {
public:
    int d1Data;
};

class Derived2 : virtual public VirtualBase {
public:
    int d2Data;
};

class FinalDerived : public Derived1, public Derived2 {
public:
    int finalData;
};

Memory Layout:

      |                        |          
      |------------------------| <------ FinalDerived class object memory layout
      |      Derived1::vptr     |  // Virtual pointer for virtual inheritance (Derived1)
      |------------------------|  
      |      Derived2::vptr     |  // Virtual pointer for virtual inheritance (Derived2)
      |------------------------|  
      |   int VirtualBase::vbData |  // Shared virtual base class data member
      |------------------------|  
      |   int Derived1::d1Data  |  // Derived1 data member
      |------------------------|  
      |   int Derived2::d2Data  |  // Derived2 data member
      |------------------------|  
      |   int FinalDerived::finalData |  // FinalDerived data member
      |------------------------|    
------|------------------------|----------------
      |   VirtualBase::vtable   |   // Virtual function pointer table
      |------------------------|    text segment
      | VirtualBase::virtualFunc()  |  // Virtual function address
      |------------------------|

Explanation:

  • Virtual Inheritance: The virtual base class VirtualBase is shared between Derived1 and Derived2. This requires separate vptrs for each derived class to manage the shared virtual base.

  • Virtual Function Dispatch: In addition to virtual inheritance, there’s also a vtable created for VirtualBase to handle the virtual function virtualFunc(). The vtable is stored in the text segment.

  • Memory Size: Each vptr occupies 8 bytes (assuming a 64-bit system). This means additional space for each derived class due to the virtual inheritance and virtual function mechanism.


Memory Layout with Alignment Considerations

Alignment ensures that data members are placed in memory according to specific boundaries for performance reasons. Misaligned data may require extra padding, depending on the architecture.

class AlignmentExample {
    char c;      // 1 byte
    int i;       // 4 bytes
    double d;    // 8 bytes
};

Memory Layout:

      |                        |          
      |------------------------| <------ AlignmentExample class object memory layout
      |     char c (1 byte)     |
      |------------------------|  
      |      Padding (3 bytes)  |  // Padding for alignment of 'int'
      |------------------------|  
      |      int i (4 bytes)    |
      |------------------------|  
      |      double d (8 bytes) |  // Naturally aligned to 8-byte boundary
      |------------------------|

Explanation:

  • Alignment and Padding: The char takes 1 byte, but since int requires a 4-byte boundary, 3 bytes of padding are added after the char to align the int. Similarly, double requires an 8-byte boundary and no additional padding is needed after int.

  • Memory Size: sizeof(AlignmentExample) will be 16 bytes (1 byte for char, 3 bytes of padding, 4 bytes for int, and 8 bytes for double).


Memory Layout with Multiple Inheritance and Virtual Destructors

Combining multiple inheritance with virtual destructors increases the complexity of memory layout because multiple base classes may contribute their own vptr for function dispatch.

class Base1 {
public:
    virtual ~Base1() {}   // Virtual destructor
    int b1;
};

class Base2 {
public:
    virtual ~Base2() {}   // Virtual destructor
    int b2;
};

class DerivedMultiple : public Base1, public Base2 {
public:
    int derivedData;
};

Memory Layout:

      |                        |          
      |------------------------| <------ DerivedMultiple class object memory layout
      |    Base1::vptr          |  // Virtual pointer for Base1's vtable
      |------------------------|  
      |    int Base1::b1        |  // Base1 data member
      |------------------------|  
      |    Base2::vptr          |  // Virtual pointer for Base2's vtable
      |------------------------|  
      |    int Base2::b2        |  // Base2 data member
      |------------------------|  
      | int DerivedMultiple::derivedData |  // Derived class data member
      |------------------------|  
------|------------------------|----------------
      |   Base1's vtable        |   // vtable for Base1
      |------------------------|    text segment
      |   Base2's vtable        |   // vtable for Base2
      |------------------------|
      |   Base1::~Base1()       |   // Destructor in Base1's vtable
      |------------------------|
      |   Base2::~Base2()       |   // Destructor in Base2's vtable
      |------------------------|

Explanation:

  • Multiple vptrs: Since both Base1 and Base2 have virtual destructors, each contributes a separate vptr. DerivedMultiple has two virtual pointers — one for Base1's vtable and another for Base2's vtable.

  • Memory Size: The presence of two vptrs (8 bytes each) adds additional memory overhead. Depending on alignment, sizeof(DerivedMultiple) will be larger than expected due to these pointers.

Memory Layout with Abstract Base Classes

Abstract base classes play a crucial role in implementing polymorphism in C++. They allow developers to define interfaces that can be inherited by derived classes, enforcing certain functionalities while also ensuring that specific implementations are provided in those derived classes. This section will expand upon the memory layout involving abstract base classes, detailing various scenarios, including multiple abstract classes, pure virtual functions, and their interactions with concrete classes.

Basic Structure of an Abstract Base Class

Consider the following abstract base class structure with both pure virtual functions and concrete implementations in derived classes:

class AbstractBase {
public:
    virtual void pureVirtualFunc() = 0; // Pure virtual function
    virtual void regularFunc() {         // Regular virtual function
        // Default implementation
    }
    int abData;                           // Data member
};

class ConcreteDerived : public AbstractBase {
public:
    void pureVirtualFunc() override {     // Implementation of pure virtual function
        // Custom implementation
    }
    void regularFunc() override {         // Overriding regular virtual function
        // Custom implementation
    }
    int cdData;                           // Additional data member
};

Memory Layout:

      |                        |          
      |------------------------| <------ ConcreteDerived class object memory layout
      |      AbstractBase::vptr |  // Virtual pointer for AbstractBase's vtable
      |------------------------|  
      |      int AbstractBase::abData |  // Data member from AbstractBase
      |------------------------|  
      |      int ConcreteDerived::cdData |  // Data member from ConcreteDerived
      |------------------------|  
------|------------------------|----------------
      |   AbstractBase's vtable  |   // vtable for AbstractBase
      |------------------------|    text segment
      |   ConcreteDerived::pureVirtualFunc() |  // Implementation in vtable
      |------------------------|
      |   ConcreteDerived::regularFunc() |  // Implementation in vtable
      |------------------------|

Explanation:

  • Virtual Pointer (vptr): The ConcreteDerived class contains a virtual pointer that points to the virtual table (vtable) of the AbstractBase class. This is necessary for dispatching virtual functions.

  • Data Members: The abData member from AbstractBase and cdData from ConcreteDerived are stored contiguously in memory.

  • Virtual Table (vtable): The virtual table holds pointers to the implementations of the virtual functions for ConcreteDerived, including both the overridden pureVirtualFunc() and regularFunc(). The presence of the pure virtual function mandates the existence of the vtable, even if AbstractBase cannot be instantiated.


Memory Layout with Multiple Abstract Base Classes

When a class inherits from multiple abstract base classes, each base class contributes its own virtual table and its corresponding data member(s). Let’s illustrate this scenario:

class AbstractBase1 {
public:
    virtual void pureVirtualFunc1() = 0; // Pure virtual function
    int ab1Data;                           // Data member
};

class AbstractBase2 {
public:
    virtual void pureVirtualFunc2() = 0; // Pure virtual function
    int ab2Data;                           // Data member
};

class ConcreteDerived : public AbstractBase1, public AbstractBase2 {
public:
    void pureVirtualFunc1() override {     // Implementation of pure virtual function
        // Custom implementation
    }
    void pureVirtualFunc2() override {     // Implementation of pure virtual function
        // Custom implementation
    }
    int cdData;                            // Additional data member
};

Memory Layout:

      |                        |          
      |------------------------| <------ ConcreteDerived class object memory layout
      |    AbstractBase1::vptr  |  // Virtual pointer for AbstractBase1's vtable
      |------------------------|  
      |    int AbstractBase1::ab1Data |  // Data member from AbstractBase1
      |------------------------|  
      |    AbstractBase2::vptr  |  // Virtual pointer for AbstractBase2's vtable
      |------------------------|  
      |    int AbstractBase2::ab2Data |  // Data member from AbstractBase2
      |------------------------|  
      |    int ConcreteDerived::cdData |  // Data member from ConcreteDerived
      |------------------------|  
------|------------------------|----------------
      |   AbstractBase1's vtable  |   // vtable for AbstractBase1
      |------------------------|    text segment
      |   ConcreteDerived::pureVirtualFunc1() |  // Implementation in vtable
      |------------------------|
      |   AbstractBase2's vtable  |   // vtable for AbstractBase2
      |------------------------|   
      |   ConcreteDerived::pureVirtualFunc2() |  // Implementation in vtable
      |------------------------|

Explanation:

  • Multiple Virtual Pointers: The ConcreteDerived class includes a virtual pointer for each abstract base class (AbstractBase1 and AbstractBase2), which means it has two vptrs pointing to their respective vtables.

  • Memory Size: Each virtual pointer takes up space (8 bytes on a 64-bit system), and the layout now includes two data members from the base classes, along with an additional data member from ConcreteDerived.

  • Virtual Tables: Each abstract base class has its own vtable to manage the dispatching of their pure virtual functions.


Memory Layout with Mixed Virtual and Non-Virtual Inheritance

Sometimes, a class hierarchy will mix virtual and non-virtual inheritance. Here’s an example to illustrate this:

class Base {
public:
    virtual void virtualFunc() = 0; // Pure virtual function
    int baseData;
};

class Derived1 : public Base {
public:
    void virtualFunc() override {     // Implementation of pure virtual function
        // Custom implementation
    }
    int d1Data;
};

class Derived2 : public Base {
public:
    void virtualFunc() override {     // Implementation of pure virtual function
        // Custom implementation
    }
    int d2Data;
};

class FinalDerived : public Derived1, public Derived2 {
public:
    int finalData;
};

Memory Layout:

      |                        |          
      |------------------------| <------ FinalDerived class object memory layout
      |      Derived1::vptr     |  // Virtual pointer for Derived1's vtable
      |------------------------|  
      |      int Base::baseData  |  // Base class data member
      |------------------------|  
      |      int Derived1::d1Data |  // Derived1 data member
      |------------------------|  
      |      Derived2::vptr     |  // Virtual pointer for Derived2's vtable
      |------------------------|  
      |      int Derived2::d2Data |  // Derived2 data member
      |------------------------|  
      |      int FinalDerived::finalData |  // FinalDerived data member
      |------------------------|  
------|------------------------|----------------
      |   Base's vtable         |   // vtable for Base
      |------------------------|    text segment
      |   Derived1::virtualFunc() |  // Implementation in vtable
      |------------------------|
      |   Derived2::virtualFunc() |  // Implementation in vtable
      |------------------------|

Explanation:

  • Multiple vptrs and Shared Base Data: The FinalDerived class contains pointers to both Derived1 and Derived2's vtables. The base class data (baseData) is stored only once, despite FinalDerived being derived from both Derived1 and Derived2.

  • Virtual Tables: Each derived class has its implementation of the pure virtual function, and the vtables facilitate correct function dispatch.

  • Memory Size: The layout includes the vptrs from both derived classes along with the base class data, leading to an increase in overall memory size.

Hope you enjoyed the article.

Keep learning : )