C++ 虚继承的对象模型

Posted chengonghao

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++ 虚继承的对象模型相关的知识,希望对你有一定的参考价值。

我们知道,虚继承的基类在类的层次结构中只可能出现一个实例。虚基类在类的层次结构中的位置是不能固定的,因为继承了虚基类的类可能会再次被其他类多继承。

 

比如class A: virtual T{} 这时T的位置如果相对于A是固定值的话,假设偏移是X,当再有个类 class B:virtual T{} ;这时假设在B里面T的偏移是固定的Y,而当再有一个类,class C: B, A {} 的时候,像这种情况,T在C里面的实例就只能有一份,由B和A共享继承,显然这时候的T的位置相对于A或者B的偏移和这两个类对象单独存在的时候的相对偏移是不可能一样的,也就是说当我们把C的指针转换成A或者B的指针的时候,通过A* 来访问T还是使用固定的偏移量X么?用B* 来访问T还是使用固定的偏移量Y么?显然如果这样做的话肯定会有一个是错的。在非虚继承的时候因为T还是由A或B独享的,偏移固定是可以的。所以在虚继承的时候虚基类的位置是变化的,位置变化的意思其实主要是指在继承层次上相对于某些子对象的偏移上,如果当一个类已经定义完毕了的话,单独对于这个类来说那么虚基类的位置就肯定是固定的了。我们要访问到这个虚基类的话还是得知道他的地址,或者说相对偏移。

 

那么怎么才能找到这个虚基类呢?C++标准里面没有规定怎么去实现虚继承等这些东西,可以由实现决定。在VC里面是使用虚基类表的方法来实现类的虚继承的。在类的层次结构中,有虚继承的话,类都会生成一个隐藏的成员变量,虚基类表指针,虚基类表指针指向一个全类共享的虚基类表,虚基类表里面每一项的单位是4个字节,第一项保存的是类本身的地址相对于虚基类表指针的偏移,第二项保存的是第一个虚基类相对于虚基类表指针的偏移,第三个依此类推。

 

我们先从一些简单的情况看下,最后在慢慢地阶段性万剑归宗。

 

class A{public:
   int a;
   A():a(0xaaaaaaaa){}
};
class B{public:
   int b;
   B():b(0xbbbbbbbb){}
};
class C: virtual A, virtual B{public:
   int c;
   C():c(0x11111111){}
};


 

我们定义一个C的变量,看下类C的内存布局是怎么样的,虚基类表又是怎么样的。

 

图1

 

上面就是变量内存的一个抓包,可以看到在最前面的肯定就是那个自动加入的隐藏成员,虚基类表指针了,我们由指针找到虚基类表,来看下虚基类表里面有什么。

虚基类表指针是0x00ef5858 (注意这是小端字节序)。


图2

 

虚基类表里面各项就是偏移了

 

1.         前4个字节:00 00 00 00,是派生类本身到虚基类表指针的偏移,可以看到是0,虚基类表指针就是在派生类C的最开始,虚基类表指针不一定都是在派生类的最开始,要不然这第一项就没意义了,后面我们会看到这种情况;

 

2.         第 5 ~ 8 字节:08 00 00 00,是第一个虚基类的偏移,可以看到是8,结合图1可知,类A(从 aa aa aa aa 开始就是类 A 的部分)相对于虚基类表指针是8字节的偏移;

 

 

3.         第9 ~ 12字节:0c 00 00 00,是B的偏移,是12,结合图1可知,类B(从 bb bb bb bb 开始就是类 B的部分)相对于虚基类表指针是12字节的偏移;

 

这就是虚基类表了,没什么神秘也没什么太多难的东西的。表里存放的东西就是固定的了,而且虚基类表是存储在只读数据区的,由所有的对象共享,也就是说,我们再创建一个C的变量,不管C的变量是临时变量还是静态或者全局变量,它的虚基类表指针还是一样的指向这个虚基类表,因为这些偏移量在类写出来的时候就已经确定了,和具体的对象是没有联系的,有人会说,那既然在类写出来后不管是实继承的类还是虚继承的类位置都是已经确定了的话那为什么还需要一个表来查找呢?这个道理很简单,和我们之前说的那个例子一样,就是在派生类的指针转换成基类的时候,这个时候如果使用这个指针来访问基类的虚继承基类的成员的话,那么这个基类的虚基类的偏移该是多少呢?这时候就会有歧义或者说冲突,总之固定下来是不可能的。而且你可以试试直接去擦写虚基类表里面的数据,看下程序会不会报错:xxx指令引用的xxx内存,该内存不能为written。 理论上是会报错的,除非你的系统是把它存储在数据区里面,这样到还可以强行擦写。


虚基类表是简单,难的是虚继承的时候类的布局情况。


我将循序渐进地推导以下的类的内存布局,逐渐给出一个我自己总结出来的通用模型,然后用这个模型去推导后几个复杂的类的每一个字节~!


#include"stdafx.h"
#pragma pack(8)
class F0{ public:char f0; F0() :f0(0xf0){} };
class F1{ public:int f1; F1() :f1(0xf1f1f1f1){} };
class F2{ public:int f2; double f22; F2() :f22(0), f2(0xf2f2f2f2){} };
 
class A : virtual F1 {
public:
       int a;
       A() :a(0xaaaaaaaa){}
};
class T : virtual F2, F0 {
public:
       int t; short t2;
       T() : t(0x11111111), t2(0x2222){}
};
class B : T{
public:
       int b; double b2;
       B() :b(0xbbbbbbbb), b2(0){}
};
class C : B,virtual T, A {
public:
       int c;
       C() :c(0x33333333){}
};
 
 
int _tmain(int argc, _TCHAR* argv[])
{
       A aa;
       T tt;
       B bb;
       C cc;
       return 0;
}


 

情况1

当然还是从简单的慢慢看起。F0、F1、F2相信都已经很清楚了,我们先看类A。

class A : virtual F1 {
public:
       int a;
       A() :a(0xaaaaaaaa){}
};


 

一个小结论:当一个类虚继承了一个基类或者多虚继承了几个基类的时候,会创建一个隐藏的成员变量,虚基类表指针,放在类的其他成员的前面。先是虚基类表指针,然后是类的各个成员,最后是各个虚基类按照继承声明的顺序依次排放在后面。

 

 

按照这个结论我们可以得出A的布局情况,A的基类都是虚基类,没有实基类,所以A先会产生一个虚基类指针指向A的虚基类表,然后就开始存放A的成员变量a,最后放虚基类F1{}的实例,因为C++要求不能破坏基类的完整性,所以F1是按照一个完整的结构放在最后的,当然这里F1只有一个int成员,所以就是一样的了。下面是A的一个实际布局我在VS2013下抓的包。


 

 

A 的虚基类表(注意是小端序)

 

 

 

可以看到,虚基类表的第一项是0(十进制),意味着派生类到虚基类表指针的距离为0,第二项是8(十进制),意为着虚基类F1距离派生类的偏移是8。这两项都是符合事实的。

 

在A的情况下,我们没有考虑结构的字节对齐的问题,当然刚好也不需要考虑,你把对齐设置成多少都是一样的内存布局,这当然是故意安排的。在下一个类T的时候我们就有必要考虑字节对齐的问题了。

 

情况2