前言
周末看资料的时候,看到虚继承和虚函数(两者完全不是一码事,正在写一个简单的总结),进一步看到C++的类对象的内存模型,网上已有很多文章,自己也记录一下。
简单模式
研究问题从简单入手,一步步深入。我们先来看一个最简单的模式;
class A{
public:
int a;
};
用测试程序可以发现,A的对象的大小是4字节,也就是a(int类型)的大小;
继承:
class E :public A
{
public:
int e;
};
此时E的对象的大小就是8(2个成员a和e的大小),在内存中的分布也很明确,a(父类成员)在前,e(子类成员)在后;
虚继承
下面来看虚继承的场景,先上代码:
class A
{
public:
int a;
};
class B :virtual public A
{
public:
int b;
};
class C :virtual public A
{
public:
int c;
};
class D :public B, public C
{
public:
int d;
};
首先来看各个类的对象占用的内存大小:
A, B, C, D:
4, 16, 16, 40
这里面涉及2个知识点,一个是虚指针和虚表,一个是内存对齐;
虚表 虚指针
首先来看虚表和虚指针:
1、虚表和虚指针只有在虚继承的情况下才会有,普通(非虚继承)继承是没有的;
2、虚指针是占用对象的空间的,虚表则是对应到类的信息里,是不占用对象空间的;
先来解释为啥B和C的对象的内存大小是16,B(或者C);
先通过代码打印一下B的对象向的地址及其成员的地址:
A a;
B b;
printf("&b = 0x%llx\n", &b);
printf("&b.a = 0x%llx\n", &b.a);
printf("&b.b = 0x%llx\n\n", &b.b);
结果如下:
&b = 0x7ffeec7657b8
&b.a = 0x7ffeec7657c4
&b.b = 0x7ffeec7657c0
我们可以做出如下猜测,对B的对象而言,其内存布局如下:
vbptr;//虚指针,size=8(64位系统),0x7ffeec7657b8,也是该对象的起始地址
b; //继承自父类的成员变量,size=4(int),0x7ffeec7657c0
a; //子类自己的成员变量,size=4(int),0x7ffeec7657c4
而对D而言,其打印结果为:
&d = 0x7ffeee9d9780
&d.a = 0x7ffeee9d97a0
&d.b = 0x7ffeee9d9788
&d.c = 0x7ffeee9d9798
&d.d = 0x7ffeee9d979c
可推测其内存布局为:
vbptr_B;//虚指针,针对父类B的,size=8(64位系统),0x7ffeee9d9780,也是该对象的起始地址
b; //继承自父类B的成员变量,size=4(int),0x7ffeee9d9788
vbptr_C;//虚指针,针对父类C的,size=8(64位系统),0x7ffeee9d9790
c; //继承自父类C的成员变量,size=4(int),0x7ffeee9d9798
d; //子类自己的成员变量,size=4(int),0x7ffeee9d979c
a; //祖父类A的成员变量,size=4(int),0x7ffeee9d97a0
内存对齐
通过上面的打印信息可以看到,在类D的对象的成员地址的打印以及后续的内存模型推理上,我们发现:
b 和 vbptr_C 两者的地址分别是:0x7ffeee9d9788 和 0x7ffeee9d9790;但b的size只有4,为啥却缺了8呢?
这就是因为内存对齐,因为vbptr_B为8字节,为了与vbptr_B保持对齐,故b也占用了8个字节,而不是它自己的实际大小:4字节