C++对象内存布局
引言
C++使用虚函数来实现多态机制,大多数编译器是通过虚函数表来实现动态绑定。
类的内存布局
1.普通类
class B {
public:
int m;
int n;
};
int main() {
printf("%ld\n", sizeof(B));
B b;
printf("%p, %p, %p\n", &b, &b.m, &b.n);
return 0;
}
类中只有普通成员变量,对象在内存中顺序存储成员变量。输出:
8
0x7ffee48dae00, 0x7ffee48dae00, 0x7ffee48dae04
2.有虚函数的类
class B {
public:
virtual void f() {
printf("B::f\n");
}
virtual void g() {
printf("B::g\n");
}
virtual void h() {
printf("B::h\n");
}
int m;
int n;
};
int main() {
printf("%ld\n", sizeof(B));
B b;
printf("%p\n%p\n%p\n", &b, &b.m, &b.n);
return 0;
}
先看输出结果:
16
0x7ffea995e540
0x7ffea995e548
0x7ffea995e54c
我们看到,这个对象的内存占用比上一个多了8个字节,其中成员变量m的地址也和对象b的地址不一样了,正好是 &b+8。
之所以这样,是因为对象中多出了虚函数指针,这个虚函数指针指向这个类的虚函数表,也就是说,这个指针保存的内容是一个虚函数表的地址,虚函数表,在内存中就是一个存储函数指针的数组
#include <stdio.h>
class B {
public:
virtual void f() {
printf("B::f\n");
}
virtual void g() {
printf("B::g\n");
}
virtual void h() {
printf("B::h\n");
}
int m;
int n;
};
using Func = void(*)(void);
int main() {
printf("%ld\n", sizeof(B));
B b;
printf("%p\n%p\n%p\n", &b, &b.m, &b.n);
long** vt = (long**)*(long***)&b;
printf("vtable addr: %lx\n", (long)vt);
Func p = nullptr;
for (int i = 0; i < 3; ++i)
{
p = (Func)*(vt + i);
p();
}
return 0;
}
输出如下:
16
0x7ffe4222ebd0
0x7ffe4222ebd8
0x7ffe4222ebdc
vtable addr: 55a2e85c3d80
B::f
B::g
B::h
图形表示为:
3.单继承无重写的类
写一个子类,继承自B类,没有重写B中的方法
#include <stdio.h>
class B {
public:
virtual void f() {
printf("B::f\n");
}
virtual void g() {
printf("B::g\n");
}
virtual void h() {
printf("B::h\n");
}
int m;
int n;
};
class D : public B {
public:
virtual void df() {
printf("D::df\n");
}
virtual void dg() {
printf("D::dg\n");
}
virtual void dh() {
printf("D::dh\n");
}
int x,y;
};
using Func = void(*)(void);
int main() {
B b;
printf("sizeof b: %ld\n", sizeof(b));
printf("addr B:\n%p\n%p\n%p\n", &b, &b.m, &b.n);
printf("vtable B: %p\n", *(long***)&b);
printf("------------\n");
D d;
printf("sizeof d:%ld\n", sizeof(d));
printf("addr D:\n%p\n%p\n%p\n%p\n%p\n", &d, &d.m, &d.n, &d.x, &d.y);
printf("vtable D: %p\n", *(long***)&d);
printf("------------\n");
long** vt = (long**)*(long***)&d;
Func p = nullptr;
for (int i = 0; i < 6; ++i)
{
p = (Func)*(vt+i);
p();
}
return 0;
}
结果:
sizeof b: 16
addr B:
0x7fffce329990
0x7fffce329998
0x7fffce32999c
vtable B: 0x559e5a8c9d68
------------
sizeof d:24
addr D:
0x7fffce3299a0
0x7fffce3299a8
0x7fffce3299ac
0x7fffce3299b0
0x7fffce3299b4
vtable D: 0x559e5a8c9d28
------------
B::f
B::g
B::h
D::df
D::dg
D::dh
这个时候内存布局变成了这样:
可以看到,子类只有一个虚函数指针,一个虚函数表,子类的函数附加在基类后面
4.单继承有重写
#include <stdio.h>
class B {
public:
virtual void f() {
printf("B::f\n");
}
virtual void g() {
printf("B::g\n");
}
virtual void h() {
printf("B::h\n");
}
int m;
int n;
};
class D : public B {
public:
virtual void f() {
printf("D::f\n");
}
virtual void dg() {
printf("D::dg\n");
}
virtual void dh() {
printf("D::dh\n");
}
int x,y;
};
using Func = void(*)(void);
int main() {
B b;
printf("sizeof b: %ld\n", sizeof(b));
printf("addr B:\n%p\n%p\n%p\n", &b, &b.m, &b.n);
printf("vtable B: %p\n", *(long***)&b);
printf("------------\n");
D d;
printf("sizeof d:%ld\n", sizeof(d));
printf("addr D:\n%p\n%p\n%p\n%p\n%p\n", &d, &d.m, &d.n, &d.x, &d.y);
printf("vtable D: %p\n", *(long***)&d);
printf("------------\n");
long** vt = (long**)*(long***)&d;
Func p = nullptr;
for (int i = 0; i < 5; ++i)
{
p = (Func)*(vt+i);
p();
}
return 0;
}
结果:
sizeof b: 16
addr B:
0x7fffbfd06b60
0x7fffbfd06b68
0x7fffbfd06b6c
vtable B: 0x56277e20cd68
------------
sizeof d:24
addr D:
0x7fffbfd06b70
0x7fffbfd06b78
0x7fffbfd06b7c
0x7fffbfd06b80
0x7fffbfd06b84
vtable D: 0x56277e20cd30
------------
D::f
B::g
B::h
D::dg
D::dh
子类重写了基类的 B::f 方法,内存布局变为:
类D的虚函数表中,第一个函数被替换成了 D::f,其他不变。
这个替换,是在编译期间完成的。
这里就是实现多态的关键地方了,当用基类的指针调用f函数,如果该指针指向的是基类,那么虚函数指针指向的就是基类的虚函数表,调用B::f。如果该指针指向子类对象,则虚函数指针指向子类的虚函数表,调用D::f
5.多重继承无重写
#include <stdio.h>
class B {
public:
virtual void f() {
printf("B::f\n");
}
virtual void g() {
printf("B::g\n");
}
virtual void h() {
printf("B::h\n");
}
int m;
int n;
};
class B2 {
public:
virtual void f() {
printf("B2::f\n");
}
virtual void g() {
printf("B2::g\n");
}
virtual void h() {
printf("B2::h\n");
}
int i,j;
};
class D : public B, public B2 {
public:
virtual void df() {
printf("D::df\n");
}
virtual void dg() {
printf("D::dg\n");
}
virtual void dh() {
printf("D::dh\n");
}
int x,y;
};
using Func = void(*)(void);
int main() {
B b;
printf("sizeof b: %ld\n", sizeof(b));
printf("addr B:\n%p\n%p\n%p\n", &b, &b.m, &b.n);
printf("vtable B: %p\n", *(long***)&b);
printf("------------\n");
B2 b2;
printf("sizeof b2: %ld\n", sizeof(b2));
printf("addr B2:\n%p\n%p\n%p\n", &b2, &b2.i, &b2.j);
printf("vtable B2: %p\n", *(long***)&b2);
printf("------------\n");
D d;
printf("sizeof d:%ld\n", sizeof(d));
printf("addr D:\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n", &d, &d.m, &d.n, &d.i, &d.j, &d.x, &d.y);
printf("vtable D: %p\n", *(long***)&d);
printf("------------\n");
// first vtable pointer
long** vt1 = (long**)*(long***)&d;
Func p = nullptr;
for (int i = 0; i < 6; ++i)
{
p = (Func)*(vt1+i);
p();
}
// second vtable pointer. member 'm' and 'n' type is int, pointer +2
long** vt2 = (long**)*((long***)&d + 2);
for (int i = 0; i < 3; ++i)
{
p = (Func)*(vt2+i);
p();
}
return 0;
}
结果:
sizeof b: 16
addr B:
0x7ffc2868cf40
0x7ffc2868cf48
0x7ffc2868cf4c
vtable B: 0x5574802e5d38
------------
sizeof b2: 16
addr B2:
0x7ffc2868cf50
0x7ffc2868cf58
0x7ffc2868cf5c
vtable B2: 0x5574802e5d10
------------
sizeof d:40
addr D:
0x7ffc2868cf60
0x7ffc2868cf68
0x7ffc2868cf6c
0x7ffc2868cf78
0x7ffc2868cf7c
0x7ffc2868cf80
0x7ffc2868cf84
vtable D: 0x5574802e5ca8
------------
B::f
B::g
B::h
D::df
D::dg
D::dh
B2::f
B2::g
B2::h
这次的内存布局为:
可以看到:
每个基类都有虚函数表;子类的函数被放到第一个第一个基类的虚函数表中。
6.多重继承有重写
#include <stdio.h>
class B {
public:
virtual void f() {
printf("B::f\n");
}
virtual void g() {
printf("B::g\n");
}
virtual void h() {
printf("B::h\n");
}
int m;
int n;
};
class B2 {
public:
virtual void f() {
printf("B2::f\n");
}
virtual void g() {
printf("B2::g\n");
}
virtual void h() {
printf("B2::h\n");
}
int i,j;
};
class D : public B, public B2 {
public:
virtual void f() {
printf("D::f\n");
}
virtual void dg() {
printf("D::dg\n");
}
virtual void dh() {
printf("D::dh\n");
}
int x,y;
};
using Func = void(*)(void);
int main() {
B b;
printf("sizeof b: %ld\n", sizeof(b));
printf("addr B:\n%p\n%p\n%p\n", &b, &b.m, &b.n);
printf("vtable B: %p\n", *(long***)&b);
printf("------------\n");
B2 b2;
printf("sizeof b2: %ld\n", sizeof(b2));
printf("addr B2:\n%p\n%p\n%p\n", &b2, &b2.i, &b2.j);
printf("vtable B2: %p\n", *(long***)&b2);
printf("------------\n");
D d;
printf("sizeof d:%ld\n", sizeof(d));
printf("addr D:\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n", &d, &d.m, &d.n, &d.i, &d.j, &d.x, &d.y);
printf("vtable D: %p\n", *(long***)&d);
printf("------------\n");
// first vtable pointer
long** vt1 = (long**)*(long***)&d;
Func p = nullptr;
for (int i = 0; i < 5; ++i)
{
p = (Func)*(vt1+i);
p();
}
// second vtable pointer. member 'm' and 'n' type is int, pointer +2
long** vt2 = (long**)*((long***)&d + 2);
for (int i = 0; i < 3; ++i)
{
p = (Func)*(vt2+i);
p();
}
return 0;
}
结果:
sizeof b: 16
addr B:
0x7ffe091424e0
0x7ffe091424e8
0x7ffe091424ec
vtable B: 0x5598cb9edd38
------------
sizeof b2: 16
addr B2:
0x7ffe091424f0
0x7ffe091424f8
0x7ffe091424fc
vtable B2: 0x5598cb9edd10
------------
sizeof d:40
addr D:
0x7ffe09142500
0x7ffe09142508
0x7ffe0914250c
0x7ffe09142518
0x7ffe0914251c
0x7ffe09142520
0x7ffe09142524
vtable D: 0x5598cb9edcb0
------------
D::f
B::g
B::h
D::dg
D::dh
D::f
B2::g
B2::h
此时的内存布局为:
基类虚函数表中的f函数都被替换成了子类的函数指针