C++对象模型Data语意学分析、虚继承底层实现机制


1. Class 的大小


  • 一个空 class 的大小为 1 字节,因为编译器需要安插进去一个 char,使得这个 class 对象得以在内存中被配置独一无二的地址。虽然空 class 大小为 1 字节,但是假如某个类 A 继承该空 class,计算类 A 的大小时会优化父类空 class 的大小,如类 A 为空,sizeof(A) = 1,不空,则为类A真实数据大小。
  • 我们通常说某个 class 内部有 virtual function 尽管没有数据成员,它的大小仍为为 4 字节,因为它有一个 vptr,指向一个 vtbl ,所以按指针算大小就是 4 字节。实际上,就算没有 virtual function,如果某个类虚继承别的类,编译器仍生成有 vtbl ,因为它的 vtbl 还要用来保存 virtual base class object(虚基类子对象)的 offset 。
  • 对于 virtual 继承,所有的 virtual base class subobject 在派生类中只保留一份,所以计算派生类对象时,按一份计算即可。
  • 计算 class 的大小时,还要考虑内存对齐,标准就是一个 bus 的长度,32 位为 4 字节。不过要注意,派生类数据成员并不会填补基类花在内存对齐上的部分。

下面举出无数例子来验证结论 :)

case 1 :class A 为空时的普通继承、普通继承有成员、虚继承、有虚函数

class A { };
class B : public A { };
class C : public A { char ch; };
class X : virtual public A { };
class Y { virtual void fun(); };

std::cout<<sizeof(A)<<std::endl;    // 1
std::cout<<sizeof(B)<<std::endl;    // 1
std::cout<<sizeof(C)<<std::endl;    // 1
std::cout<<sizeof(X)<<std::endl;    // 4
std::cout<<sizeof(Y)<<std::endl;    // 4
输出结果分析:A 为空,除了它自己,别人都会优化掉它。虚继承和含有虚函数这两种情况都有 vptr,所指东西不同而已。 当然 vptr 在既有虚继承,又有虚函数的类中正负分别指向 virtual functions 和 virtual base class subobject 的 offset,只有一个 vptr。(Mircosoft 编译器有两个虚函数表 vtbl ,有两个 vptr;G++实际上只有一个 vtbl,一个vptr,并且 virtual funcions 和 virtual base class subobject 的 offset 按顺序存放)。

g++与虚继承

g++编译器生成的C++类实例,虚函数与虚基类地址偏移值共用一个虚表(vtable)。类实例的开始处即为指向所属类的虚指针(vptr)。实际上,一个类与它的若干祖先类(父类、祖父类、...)组成部分共用一个虚表,但各自使用的虚表部分依次相接、不相重叠。

g++编译下,一个类实例的虚指针指向该类虚表中的第一个虚函数的地址。如果该类没有虚函数(或者虚函数都写入了祖先类的虚表,覆盖了祖先类的对应虚函数),因而该类自身虚表中没有虚函数需要填入,但该类有虚继承的祖先类,则仍然必须要访问虚表中的虚基类地址偏移值。这种情况下,该类仍然需要有虚表,该类实例的虚指针指向类虚表中一个值为0的条目。

该类其它的虚函数的地址依次填在虚表中第一个虚函数条目之后(内存地址自低向高方向)。虚表中第一个虚函数条目之前(内存地址自高向低方向),依次填入了typeinfo(用于RTTI)、虚指针到整个对象开始处的偏移值、虚基类地址偏移值。因此,如果一个类虚继承了两个类,那么对于32位程序,虚继承的左父类地址偏移值位于vptr-0x0c,虚继承的右父类地址偏移值位于vptr-0x10.

一个类的祖先类有复杂的虚继承关系,则该类的各个虚基类偏移值在虚表中的存储顺序尊重自该类到祖先的深度优先遍历次序。

Microsoft Visual C++与虚继承

Microsoft Visual C++与g++不同,把类的虚函数与虚基类地址偏移值分别放入了两个虚表中,前者称为虚函数表vftbl,后者称虚基类表vbtbl。因此一个类实例可能有两个虚指针分别指向类的虚函数表与虚基类表,这两个虚指针分别称为虚函数表指针vftbl与虚基类表指针vbtbl。当然,类实例也可以只有一个虚指针,或者没有虚指针。虚指针总是放在类实例的数据成员之前,且虚函数表指针总是在虚基类表指针之前。因而,对于某个类实例来说,如果它有虚基类指针,那么虚基类指针可能在类实例的0字节偏移处,也可能在类实例的4字节偏移处(对于32位程序来说),这给类成员函数指针的实现带来了很大麻烦。

一个类的虚基类指针指向的虚基类表的首个条目,该条目的值是虚基类指针到整个类实例内存首地址的偏移值。即obj.vbtbl - &obj。虚基类第2、第3、... 个条目依次为该类的最左虚继承父类、次左虚继承父类、...的内存地址相对于虚基类表指针自身地
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值