您的位置:首页 > 编程语言 > C语言/C++

深度探索C++对象模型--继承与Data Member

2014-08-08 01:28 459 查看
在C++继承模型中,一个继承类所表现出的东西,是自己的member与其加上基类成员的总和,而且排列顺序一般是基类在上(虚基类除外)。

假设Point2d, Point3d无继承关系,数据分布如下图:



以下分四种关系:单一继承不含virtual, 单一继承含有virtual, 多重继承,虚拟继承,分析每种的数据分布。

1 只要继承不含virtual(单一继承):这种继承并不会增加空间或存取时间上的额外负担。如果Point2d共有继承自Point3d,数据分布如下:



如上图,Point3d包含了一个Point2d子对象,C++保证出现在继承类中的基类子对象保持其原有的完整性,是为了保证指针赋值之间表现正确,如果为了节省空间,不保持原样,因为有padding等等,所以指针赋值可能会出现错误。覆盖掉原来的值,但还有多余的值。

2 加上多态:每个对象都会多一个vptr,指向虚函数表。

假如Point2d含有虚函数,为了支持多态,在这种情况下会带来空间与存取时间上的额外负担,如下:

(1)导入一个和Point2d有关的virtual table,用来存放所声明的每一个虚函数地址,元素个数一般是声明的虚函数个数,加上为了支持RTTI的一个或两个slots

(2)在每个类对象中导入一个vptr,提供执行期链接,使其找到virtual table

(3)加强constructor,使它设定vptr初值,而且可能意味着在derived class和每一个base class的构造函数中都重新设定vptr的值

(4)加强destructor,即取消vptr的值。

关于vptr的位置,一般是放在最上或最下,virtual c++放在最上面,这个在通过类对象的指针调用虚函数时会带来帮助,但是丧失了与C的兼容性。一种是尾端,如gcc,这保持了兼容,但对虚函数调用可能要一些额外操作。下图是vptr放在对象最下面 加入多态后的数据分布如下:



3 多重继承。问题主要发生在derived class objects和其第二个及其后的基类对象之间的转换。
Point3d p3d;
Point2d *p = &p3d;


上面的转换因为是单一继承,转换是直接的。单一继承提供了自然多态,即base type与derived type之间的类型转换是直接的,因为两者的对象都是从相同的地址开始的,差异只是derived对象较大。这个赋值操作只需执行指针赋值,就可以完全正确,且有最佳执行效率。(这种的前提是如果有vptr,vptr放在对象尾部)

对于一个多重继承,问题发生在derived class objects和其第二个及其后的基类对象之间的转换。

如果将多重派生对象地址赋给第一个基类,因为地址相同,所以与单一继承时相同,因为二者有相同的起始地址。

如果将多重派生对象地址赋给第二个或其后继子类,则需要将这个派生对象的地址修改,加上(或减去,如果是downcast)的介于中间的base class subobject(s)的大小。

如果有下面的继承关系:



对象布局如下(vptr在下):



对于上面这些类之间的赋值的操作如下:
Vertex3d v3d;
Vertex  *pv;
Point2d *p2d;
Point3d *p3d;

pv = &v3d;
//转换可能如下:
pv = (Vertex*)(((char*)&v3d)+sizeof(Point3d));
//而下面的操作,只需要简单拷贝:
p2d = &v3d;
p3d = &v3d;

//如下两个指针,可能要进行判断
Vertex3d *pv3d;
Vertex *pv;
pv = pv3d;
//转换如下,防止pv3d为0
pv = pv3d ? (Vertex*)((char*)pv3d+sizeof(Point3d)) : 0;


引用不需要对0做防卫,因为引用不可能参考“无物”。

4 虚拟继承:共享的基类在每个类中只含有一份。

如下图关系:



要解决的问题:Vertex, Point3d都含有一份Point2d,而Vertex3d需要将这两个子对像中的Point2d子对象合成为一份,并且还要保持基类与继承类的指针之间多态的赋值。

一般实现方法:首先如果类含有一个或多个虚基类,先分割成两部分,一个不变区域和一个共享区域。不变区域中的数据,无论后继如何变化,总有固定的offset(从object的开头算起),所以这部分数据可以直接存取。共享区域,就是虚基类子对像,位置会因为每次派生操作发生变化,所以只能间接存取。所以编译器的差异在于间接存取的方式。

目前有两种实现模型:

(1)在每个derived class object中安插一些指针,每个指针指向virtual base class.所以存取虚基类对象,可以通过指针完成。这种模型的图如下:



在这种模型下,一个继承类与基类之间的转化:

Point2d *p2d = pv3d;
//虚拟c++代码,转换为以下形式
Point2d *p2d = pv3d ? pv3d->vbcPoint2d : 0;
这个模型有主要有以下两个缺点:

(1)每个对象必须针对每个virtual base class背负一个额外的指针

(2)由于虚拟串链的加长,导致间接存取层次的增加

对于第一个问题,Microsoft的编译器引用了一个virtual class class table,即类似与虚函数表,编译器安插一个指针指向该表,表中的slot指向真正的虚基类地址。

第二个解决方法,是在virtual function table中放置virtual base class的offset(不是地址),即将虚函数偏移地址与虚函数表混在一起。Sun编译器中,virtual function table可经由正值或负值来索引,正表示虚函数,父表示虚基类。该模型如下图:



在该模型下,指针转化如下:

Point2d *p2d = pv3d;
//虚拟c++代码,转换为以下形式
Point2d *p2d = pv3d ? pv3d+pv3d->__vptr__Point3d[-1] : 0;


以上是发生在经由指针来存取一个继承来的虚基来的成员,如果是经过对象存取,可以直接优化为一个直接存取操作,以上问题也不会存在。

总结呀:virtual base class最有效的一种运用形式是:如果一个抽象的virtual base class,最好没有任何data成员。。。。

PS:

明天要起不来了,怎么办??

</pre></p><p>           </p><p>          </p><p>       </p><p>        <pre name="code" class="cpp">
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: