C++ Object Memory Layout

This post looks at the memory layout of C++ objects under various cases of inheritance. This is all platform-dependent; I am on x64 Linux and using the GCC compiler. Note that vtables are not mandated by the standard, but are an implementation detail of the compiler. Compiler optimizations are turned off.

Play with it on: Compiler Explorer

No Virtual Methods

class Base {
public:
    void Foo() const {
        printf("x = %d\n", x);
    }

    int32_t x;
};

class Child : public Base {
public:
    int32_t y;
};

int main() {
    Child* c = new Child;
    c->Foo();

    Base* b = c;

    printf("c = %p\n", c);          // Some address.
    printf("b = %p\n", b);          // Same as c.
    printf("&b->x = %p\n", &b->x);  // Same as c.
    printf("&c->x = %p\n", &c->x);  // Same as c.
    printf("&c->y = %p\n", &c->y);  // c + 4 bytes

    delete c;
    return 0;
}

The memory layout:

0       4         8
[Base::x][Child::y]
^
c == b

Allocating the Child object:

mov     edi, 8                       # Child is 8 bytes.
call    operator new(unsigned long)
mov     QWORD PTR [rbp-8], rax       # [rbp-8] is where the pointer c is stored
                                     # on the stack.

Call to Base::Foo():

mov     rax, QWORD PTR [rbp-8]  # c, or 'this' inside the method.
mov     rdi, rax
call    Base::Foo() const

Virtual Base

Making the Base destructor virtual is sufficient. Note that we have not made Base::Foo() virtual yet.

class Base {
public:
    virtual ~Base() {}

    void Foo() const {
        printf("x = %d\n", x);
    }

    int32_t x;
};

class Child : public Base {
public:
    int32_t y;
};

int main() {
    Child* c = new Child;
    c->Foo();

    Base* b = c;

    printf("c = %p\n", c);          // Some address.
    printf("b = %p\n", b);          // Same as c.
    printf("&b->x = %p\n", &b->x);  // b +  8 bytes.
    printf("&c->x = %p\n", &c->x);  // c +  8 bytes.
    printf("&c->y = %p\n", &c->y);  // c + 12 bytes.

    delete c;
    return 0;
}

Memory layout:

0                8        12       16
[   vtable ptr   ][Base::x][Child::y]
^
c == b

The Child object pointed to by c now gets a vtable pointer, which is 8 bytes on a 64-bit system. This makes Child a total of 16 bytes, as we can see in the code generated to allocate the object:

mov     edi, 16                      # Child is 16 bytes.
call    operator new(unsigned long)

These are the vtables for Base and Child:

vtable for Child:
        .quad   0
        .quad   typeinfo for Child
        .quad   Child::~Child() [complete object destructor]
        .quad   Child::~Child() [deleting destructor]
vtable for Base:
        .quad   0
        .quad   typeinfo for Base
        .quad   Base::~Base() [complete object destructor]
        .quad   Base::~Base() [deleting destructor]

Nothing else changes significantly.

Virtual Base Method

class Base {
public:
    virtual ~Base() {}

    virtual void Foo() const {
        printf("x = %d\n", x);
    }

    int32_t x;
};

class Child : public Base {
public:
    int32_t y;
};

Child is still 16 bytes. Program output remains the same (modulo address where the Child object pointed to by c is allocated).

The vtables each get a pointer to the Base::Foo() method:

vtable for Child:
        .quad   0
        .quad   typeinfo for Child
        .quad   Child::~Child() [complete object destructor]  # <--- vtable address
        .quad   Child::~Child() [deleting destructor]
        .quad   Base::Foo() const                             # <--- +16 bytes
vtable for Base:
        .quad   0
        .quad   typeinfo for Base
        .quad   Base::~Base() [complete object destructor]    # <--- vtable address
        .quad   Base::~Base() [deleting destructor]
        .quad   Base::Foo() const                             # <--- +16 bytes

The call to Base::Foo() now requires a vtable lookup:

mov     rax, QWORD PTR [rbp-24]  # rax = c.
mov     rax, QWORD PTR [rax]     # rax = pointer to vtable.
add     rax, 16                  # offset by 16 bytes, see vtable above.
mov     rdx, QWORD PTR [rax]     # read Foo() function pointer from vtable (Base::Foo()).
mov     rax, QWORD PTR [rbp-24]  # c, or 'this' inside the method.
mov     rdi, rax
call    rdx                      # call Base::Foo().

Virtual Base Method, Child Method Override

class Base {
public:
    virtual ~Base() {}

    virtual void Foo() const {
        printf("x = %d\n", x);
    }

    int32_t x;
};

class Child : public Base {
public:
    void Foo() const override {
        printf("x = %d, y = %d\n", x, y);
    }

    int32_t y;
};

Again, Child is still 16 bytes and there is nothing different in terms of program output except absolute addresses.

Child now defines its own Foo() override. This is reflected in the vtables:

vtable for Child:
        .quad   0
        .quad   typeinfo for Child
        .quad   Child::~Child() [complete object destructor]  # <--- vtable address.
        .quad   Child::~Child() [deleting destructor]
        .quad   Child::Foo() const                            # <--- +16 bytes.
vtable for Base:
        .quad   0
        .quad   typeinfo for Base
        .quad   Base::~Base() [complete object destructor]    # <--- vtable address.
        .quad   Base::~Base() [deleting destructor]
        .quad   Base::Foo() const                             # <--- +16 bytes.

In the previous section, the Child class did not define its own Foo() override, so the Child vtable pointed to Base::Foo(). Here, we have defined a Child::Foo() override, so the Child vtable now points to Child::Foo() instead of Base::Foo(). The Base vtable has not changed.

The generated code to call c->Foo() is identical. The difference is only in the vtable contents. c->Foo() calls Child::Foo() by virtue of the Child vtable definition:

mov     rax, QWORD PTR [rbp-24]  # rax = c.
mov     rax, QWORD PTR [rax]     # rax = pointer to vtable.
add     rax, 16                  # offset by 16 bytes, see vtable above.
mov     rdx, QWORD PTR [rax]     # read Foo() function pointer from vtable (Child::Foo()).
mov     rax, QWORD PTR [rbp-24]  # rax = c, or 'this' inside the method.
mov     rdi, rax                 # rdi = c
call    rdx                      # call Base::Foo().

Non-Virtual Base, Child Virtual Method

A slight twist on the very first code snippet; keep Base non-virtual, but give Child a virtual method:

class Base {
public:
    int32_t x;
};

class Child : public Base {
public:
    virtual void Foo() const {
        printf("x = %d, y = %d\n", x, y);
    }

    int32_t y;
};

int main() {
    Child* c = new Child;
    c->Foo();

    Base* b = c;

    printf("c = %p\n", c);          // Some address.
    printf("b = %p\n", b);          // c + 8 bytes (skip vtable ptr).
    printf("&b->x = %p\n", &b->x);  // Same as b.
    printf("&c->x = %p\n", &c->x);  // Same as b.
    printf("&c->y = %p\n", &c->y);  // c + 12 bytes (equivalently, b + 4 bytes).

    delete c;
    return 0;
}

Note that c and b no longer point to the same address in memory. This is the memory layout and the pointers now:

0                8        12       16
[   vtable ptr   ][Base::x][Child::y]
^                ^
c                b

Base does not get a vtable because it has no virtual methods. Child does get one:

vtable for Child:
        .quad   0
        .quad   typeinfo for Child
        .quad   Child::Foo() const  # <--- vtable address.

The call to c->Foo() requires a vtable lookup, compiler optimizations aside:

mov     rax, QWORD PTR [rbp-24]  # rax = c
mov     rax, QWORD PTR [rax]     # rax = vtable ptr
mov     rdx, QWORD PTR [rax]     # address of Child::Foo().
mov     rax, QWORD PTR [rbp-24]  # rax = c, or 'this' inside the method.
mov     rdi, rax                 # rdi = c.
call    rdx                      # call Child::Foo().

The address of Child::Foo() is the first (and only) function pointer in the Child vtable, which is why we see no offseting like in the prior examples.