【深入探索c++对象模型】data语义学二
单一继承中,base class 和derived class的对象都是从相同的地址开始,其间差异只在于derived class比较大,用以容纳自己的nonstatic members。
若vptr放在class object的起始处,如果base class没有虚函数而derived class有,那么单一继承的上述机制就被打破,把一个derived object转换为其base类型,就需要编译器的介入,用于调整地址。
虚拟继承
class ios1 { int a; }; class istream1 :public ios1 { int b; }; class ostream1 :public ios1 { int c; }; class iostream1 :public istream1, public ostream1 { int d; }; void main() { cout << sizeof(ios1) << " " << sizeof(istream1) << " " << sizeof(ostream1) << " " << sizeof(iostream1); system("pause"); } //vs2013输出为4 8 8 20
class ios1 { int a; }; class istream1 :public virtual ios1 { int b; }; class ostream1 :public virtual ios1 { int c; }; class iostream1 :public istream1, public ostream1 { int d; }; void main() { cout << sizeof(ios1) << " " << sizeof(istream1) << " " << sizeof(ostream1) << " " << sizeof(iostream1); system("pause"); } //输出为4 12 12 24
各类经由拷贝操作取得所有的nested virtual base class指针,放到derived class object之中。所以上述代码中的24为4个int成员的16个字节和继承自两个直接父类的指针8个字节。istream和ostream中都含有一个ios subobject,为了让iostream的对象布局中只有一个ios subobject,使用虚拟继承。
如何合并istream和ostream中的共同部分?
class如果内含一个或多个virtual base class subobjects,像istream那样,将被分割为两部分:一个不变局部和一个共享局部。不变局部中的数据,不管后继如何演变,总是拥有固定的offset,所以这一部分数据可以直接存取。共享局部(表现virtual base class subobject),其位置会因为每次的派生操作而有变化,所以只可以被间接存取(指针)。
一般的布局策略是先安排好派生类的不变部分,然后再建立其共享部分。
cfront编译器会在每一个派生类对象中安插一些指针,每个指针指向一个virtual base class,要存取继承得来的virtual base class members,可以通过相关指针间接完成
例子:
void point3d::operator+=(const point3d &rhs) { _x+=rhs._x; _y+=rhs._y; _z+=rhs._z; } 在cfront策略下,这个运算符被内部转换为: _vbcPoint2d->_x+=rhs._vbcPoint2d->_x; _vbcPoint2d->_y+=rhs._vbcPoint2d->_y; _z+=rhs._z;
以上模型有两个缺点:
1.每一个对象必须针对其每一个virtual base class背负一个额外的指针
2.由于虚继承串链的加长,导致间接存取层次的增加
为了第二个问题,编译器经由拷贝操作取得所有的nested virtual base class指针,放到derived class object之中,以空间换取时间。
为了不让对象因为其virtual base class的数目的变化导致指针数目变化,进而改变存取所需空间,可以引入virtual base class table,每一个class object如果有一个或多个virtual base class,就会由编译器安插一个指针,指向virtual base class table。该table中存放指向virtual base class的指针(microsoft编译器实现)。
第二个解决方法是在virtual function table中放置virtual base class 的offset。virtual function table可经由正值或负值来索引,如果是正值,就索引到virtual function,如果是负值,则索引到virtual base class offset。
虚拟基类是为解决多重继承而出现的。如:类D继承自类B1、B2,而类B1、B2都继承自类A,因此在类D中两次出现类A中的变量和函数。为了节省内存空间,可以将B1、B2对A的继承定义为虚拟继承,而A就成了虚拟基类。虚拟继承在一般的应用中很少用到,所以也往往被忽视,这也主要是因为在C++中,多重继承是不推荐的,也并不常用,而一旦离开了多重继承,虚拟继承就完全失去了存在的必要因为这样只会降低效率(通过指针来间接存取虚基类的成员)和占用更多的空间。
笔试,面试中常考的C++虚拟继承的知识点:
第一种情况: 第二种情况: 第三种情况 第四种情况:
class a class a class a class a
{ { { {
virtual void func(); virtual void func(); virtual void func(); virtual void func();
}; }; char x; char x;
class b:public virtual a class b :public a }; };
{ { class b:public virtual a class b:public a
virtual void foo(); virtual void foo(); { {
}; }; virtual void foo(); virtual void foo();
}; };
四种情况分别求sizeof(a),sizeof(b),结果如下:
第一种:4,12
第二种:4,4
第三种:8,16
第四种:8,8
若是虚继承,派生类通过vbptr指向父类,派生类大小包括自己的vptr,vbptr,以及由vbptr指向的父类的大小;普通继承,派生类大小包括父类的数据成员以及自己的vptr
多重继承
对一个多重派生对象,将其地址指定给最左端base class的指针,情况将和单一继承时相同,因为二者都指向相同的起始地址,需付出的成本只有地址的指定操作而已。至于第二个或后继的base class的地址指定操作,则需要将地址修改过:加上介于中间的base class subobject大小。
注意:存取第二个(或后继)base class中的一个data member,不需要额外的成本,member的位置在编译时就固定了。不管是经由指针、引用还是object来存取
class X { public: static void fun() { printf("%d\n", &X::a); printf("%d\n", &X::b); printf("%d\n", &X::m); printf("%d\n", &X::n); printf("%d\n", &X::x); printf("%d\n", &X::y); } int a; int b; protected: int m; int n; private: int x; int y; }; int main() { X::fun(); system("pause"); return 0; }
输出为0,4,8,12,16,20
取某个成员的地址,表示该成员在class object中的偏移量,c++要求同一个access level中的members在内存中的排列次序应该和其声明次序相同,vptr在vs2013中应该是放到对象的尾部了。
参考:
http://blog.csdn.net/littlehedgehog/article/details/5442430
http://blog.csdn.net/hyg0811/article/details/11951855#
http://blog.csdn.net/wangqiulin123456/article/details/8059536