以Fruit和Apple为例进行分析:
Fruit和Apple的定义如下:
通过在两种编译环境下的测试(GNU GCC & VS2015),可以发现这两种编译器的对象模型是一样的,如下图所示:
Apple是Fruit的子类,此为两级的单链继承结构。在Apple和Fruit对象内部,均遵循以下原则:
- 对象中的第一个成员是指向虚表的虚指针;
- 对象是按照声明中的顺序被保存的;
然而,两种编译器的内存的位对齐方式略有不同。
对于GNU GCC编译器而言,其遵循以下的原则:
- 按声明中出现的顺序进行内存分配
- 变量的起始偏移地址必须是自己大小的倍数,例如在struct{bool a;double b;}中,a占1个字节,然而b的大小是8个字节,因此b必须从编译地址为8的位置看是,也就是说,a和b要空7个字节。其之所以这样设计,是因为在读取一个变量时,这种方式读取可以使cache速度更快。
- 如果类中存在虚函数,在对象的起始处会有虚指针。
- 在所有变量的内存分配结束后,对象要填补成内存中的最大的基本类型变量的倍数。例如,如果一个类中最大的基本类型是double,那么它最后需要填补成8的整数倍。
还有三个特点在Fruit和Apple的关系中没有涉及到,他们是:
- 多重继承的情况下,在每个基类的前边上会有不同的vptr;
- 如果在派生类中存在新的虚函数,则会产生一个兼容基类的虚表,而不会添加新的表;
- 组合关系时,内部类的起始地址应从内部类的最大的基本数据类型的整数倍处开始。
综合前4个特点,可以计算得到在GNU GCC的编译环境下,Fruit的大小是(4+4+8+(1+7))=24Bytes; Apple的大小是(24+4+(1+3))=32Bytes,如下图:
然而,在VS2015的编译环境下,虚指针位对齐的方式是不同的。VS2015中要求数据成员的起始地址也必须是内部最大基本数据类型的整数倍,也就是说,在虚指针和数据成员之间必须存在4个占位字节,因此Fruit的大小是((4+4)+(4+4)+8+(1+7))=32Bytes;而Apple的大小是(32+4+(1+4))=40;
如图所示,尽管指针变量的大小为4字节,no的偏移量仍然是从8开始的。
Apple的内存截图如下:
前32个字节:
后8个字节:
其中,0x649bfb00位虚指针地址,0xcc为占位符,第一个0xffffffff为no,0x0000000000000000为weight,0x01为key。
注:如果将double变量删去,则第一个数据成员从4开始,也就是说Fruit的大小应该变为(4+4+(1+3))=12,如图:
内存分配图如下:
这验证了VS2015编译器的额外条件。