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.