首页 > 代码库 > C++ 继承、多继承、虚拟继承对象模型
C++ 继承、多继承、虚拟继承对象模型
C++面向对象语言一大难点是继承,但又是不得不掌握的。简单的继承是很容易理解的,但是当涉及到多继承,设计到虚函数的继承,特别是涉及到虚继承时,问题就会变得复杂。下面的内容来自参考资料中的三篇文章。C++的继承学习中,最主要是要掌握派生类的对象模型,基类和派生类指针之间的向上向下类型转换,当继承中的出现虚函数成员函数的访问(多态),虚继承是如何通过引入虚基表解决“菱形继承”中存在多份公共基类的问题。
一、简单的对象模型
1.定义
class MyClass { public: int var; void foo(){} //普通成员函数 virtual void fun() {} //虚拟成员函数 };
MyClass对象大小是8个字节。前四个字节存储的是虚函数表的指针vfptr,后四个字节存储对象成员var的值。虚函数表的大小为4字节,就一条函数地址,即虚函数fun的地址,它在虚函数表vftable的偏移是0。因此,MyClass对象模型的结果如图下图所示(在64位机器中,指针的大小是8个字节。所以MyClass对象大小是应该是16个字节。前8个字节存储的是虚函数表的指针vfptr,后四个字节存储对象成员var的值,加上内存对齐是16个字节)
MyClass的虚函数表虽然只有一条函数记录,但是它的结尾处是由4字节的0作为结束标记的。
adjust表示虚函数机制执行时,this指针的调整量,假如fun被多态调用的话,那么它的形式如下:
*(this+0)[0]()
总结虚函数调用形式,应该是:
*(this指针+调整量)[虚函数在vftable内的偏移]()
二、单继承
定义派生类MyclassA,继承自MyClass类,重写了foo(),fun(),定义了funA()。
在单继承形式下,子类的完全获得父类的虚函数表和数据。子类如果重写了父类的虚函数(如fun),就会把虚函数表原本fun对应的记录(内容MyClass::fun)覆盖为新的函数地址(内容MyClassA::fun),否则继续保持原本的函数地址记录。如果子类定义了新的虚函数,虚函数表内会追加一条记录,记录该函数的地址(如MyClassA::funA)。它的对象模型如下图所示。
class MyClassA: public MyClass { public: int varA; void foo(){ } virtual void fun() { } virtual void funA() { } };
非虚继承下的向上向下类型转换是容易的,使用静态转换完成。当派生类重写了基类的方法时,普通成员函数的访问取决于指针的类型,虚函数的访问取决于指针所指向的内容。对于非虚的成员函数来说,调用哪个成员函数是在编译时,根据“->”操作符左边指针表达式的类型静态决定的。对于虚函数调用来说,调用哪个成员函数在运行时 决定。不管“->”操作符左边的指针表达式的类型如何,调用的虚函数都是由指针实际指向的实例类型所决定。(这就是c++中的多态)。为了实现这种机制,引入了隐藏的vfptr 成员变量。 一个vfptr被加入到类中(如果类中没有的话),该vfptr指向类的虚函数表(vftable)。类中每个虚函数在该类的虚函数表中都占据一项。每项保存一个对于该类适用的虚函数的地址。因此,调用虚函数的过程如下:取得实例的vfptr;通过vfptr得到虚函数表的一项;通过虚函数表该项的函数地址间接调用虚函数。也就是说,在普通函数调用的参数传递、调用、返回指令开销外,虚函数调用还需要额外的开销。
例如下例子中:
- 普通成员函数foo()函数的调用,无论对象的内容是什么,derivaed->foo()始终调用派生类中的foo函数,base->foo()始终调用基类中foo函数。
- 虚函数调用取决于指针所指向的对象类型,例如第一个base->fun(),因为base指向的是一个派生对象,调用的是派生类中的fun()方法。同类向下转换中derivedA->fun()调用的是基类中的fun()函数。
//向上转换 MyClassA* derivedA= new MyClassA(); MyClass* base = static_cast<MyClass*>(derivedA); derivedA->foo(); //MyClassA:foo() derivedA->fun(); //MyClassA:fun() base->foo(); //MyClass:foo() base->fun(); //MyClassA:fun() 多态 //向下转换base = new MyClass(); derivedA = static_cast<MyClassA*>(base); base->foo(); //MyClass:foo() base->fun(); //MyClass:fun() derivedA->foo(); //MyClassA:foo() derivedA->fun(); //MyClass:fun()
三、多继承
为了介绍,定义MyClassB,MyClassB和MyClassA类似,就不赘述了。同时也定义一个MyClassC同时继承自MyClassA、MyClassB。
class MyClassB: public MyClass { public: int varB; void foo(){} virtual void fun() {} virtual void funB(){} }; class MyClassC : public MyClassA,public MyClassB{ public: int varC; void foo(){} virtual void funB(){} virtual void funcC(){} };
和单重继承类似,多重继承时MyClassC会把所有的父类全部按序包含在自身内部,而且每一个父类都对应一个单独的虚函数表。
多重继承下,子类不再具有自身的虚函数表,它的虚函数表与第一个父类的虚函数表合并了。同样的,如果子类重写了任意父类的虚函数,都会覆盖对应的函数地址记录。如果MyClassC重写了fun函数(两个父类都有该函数),那么两个虚函数表的记录都需要被覆盖!在这里我们发现MyClassC::funB的函数对应的adjust值是12,按照我们前边的规则,可以发现该函数的多态调用形式为:
*(this+12)[1]()
此处的调整量12正好是MyClassB的vfptr在MyClassC对象内的偏移量。(32位的机器)
多继承和单继承一样都可以使用静态转换完成向下的类型转换,但是转换到不同类型,指针的地址是不同的。
MyClassC* ddc = new MyClassC(); MyClassA* da = static_cast<MyClassA*>(ddc); MyClassB* db = static_cast<MyClassB*>(ddc); cout << ddc << endl; //0x11a7c20 cout << da << endl; //0x11a7c20,第一个继承类MyClassA cout << db << endl; //0x11a7c30,第二个继承类MyClassB
另外,在这个多继承关系中,两个继承类MyClassA和MyClassB都继承类MyClass类,这种继承关系也被称为”菱形“继承。
细心的读者会发现,派生类存在多份,公共基类的成员,本样例中的var成员变量。
菱形继承关系中,直接向上转换成公共基类会是不可以的,也无法直接访问公共基类的成员,因为在编译器看来都是模糊的行为,不知道来自MyclassA还是MyClassB,编译器都会报错
MyClass* base = static_cast<MyClass*>(ddc); cout << ddc->var << endl;
error: ‘MyClass’ is an ambiguous base of ‘MyClassC’
error: request for member ‘var’ is ambiguous
正确的做法要指明来自哪个继承类,避免产生歧义
MyClass* base = static_cast<MyClass*>(static_cast<MyClassA*>(ddc)); cout << ddc->MyClassA::var << endl;
四、虚继承
C++中为了避免菱形继承中,派生类存在多费公共基类的拷贝问题,引入了虚继承的概念。
虚继承中引入了虚基表指针,使得继承问题变得更加复杂。虽然在单继承中,一般不会出现虚继承,但是为了一步一步的理解虚继承还是从单继承的虚继承开始。
使用虚继承非常简单,只需要在普通继承前面加上virtual关键字就可以啦,例如MyClassA虚继承自MyClass类:
class MyClassA: virtual public MyClass { public: int varA; void foo(){ } virtual void fun() { } virtual void funA() { } };
MyClassA对象的内存布局,MyClassA类的大小在VS64位平台下是40个字节,GCC64位平台32个字节。这两种在虚继承上的实现可能有些不同,这里以VS编译器为类介绍MyClassA对象的模型。下面是使用vs2012看到到MyClassA的
class MyClassA size(40):
1> +---
1> 0 | {vfptr}
1> 8 | {vbptr}
1> 16 | varA
1> | <alignment member> (size=4)
1> +---
1> +--- (virtual base MyClass)
1> 24 | {vfptr}
1> 32 | var
1> | <alignment member> (size=4)
1> +---
1>
1> MyClassA::$vftable@MyClassA@:
1> | &MyClassA_meta
1> | 0
1> 0 | &MyClassA::funA
1>
1> MyClassA::$vbtable@:
1> 0 | -8
1> 1 | 16 (MyClassAd(MyClassA+8)MyClass)
1>
1> MyClassA::$vftable@MyClass@:
1> | -24
1> 0 | &MyClassA::fun
1>
1> MyClassA::fun this adjustor: 24
1> MyClassA::funA this adjustor: 0
转换成图形,如下图所示,vbtable中的-8表示第一个vfptr与vbptr之间的偏移差,16表示第二个vfptr与vbptr之间的偏移差。
我们所过虚继承最大的意义在于“菱形继承”中避免派生类拥有多份公共积累的内容。同类重新定义MyClassB虚继承自MyClass,MyClassA继承自MyClassA和MyClassB。
MyClassB的内存结构和MyClassA的内存结构类似,不再赘述了。主要分析MyClassC的内存结构。
class MyClassB: virtual public MyClass { public: int varB; void foo(){} virtual void fun() {} virtual void funB() {} }; class MyClassC : public MyClassA,public MyClassB{ public: int varC; void foo(){} virtual void funB(){} virtual void funC(){}
virtual void fun(){} };
1> class MyClassC size(72):
1> +---
1> | +--- (base class MyClassA)
1> 0 | | {vfptr}
1> 8 | | {vbptr}
1> 16 | | varA
1> | | <alignment member> (size=4)
1> | +---
1> | +--- (base class MyClassB)
1> 24 | | {vfptr}
1> 32 | | {vbptr}
1> 40 | | varB
1> | | <alignment member> (size=4)
1> | +---
1> 48 | varC
1> | <alignment member> (size=4)
1> +---
1> +--- (virtual base MyClass)
1> 56 | {vfptr}
1> 64 | var
1> | <alignment member> (size=4)
1> +---
1>
1> MyClassC::$vftable@MyClassA@:
1> | &MyClassC_meta
1> | 0
1> 0 | &MyClassA::funA
1> 1 | &MyClassC::funC
1>
1> MyClassC::$vftable@MyClassB@:
1> | -24
1> 0 | &MyClassC::funB
1>
1> MyClassC::$vbtable@MyClassA@:
1> 0 | -8
1> 1 | 48 (MyClassCd(MyClassA+8)MyClass)
1>
1> MyClassC::$vbtable@MyClassB@:
1> 0 | -8
1> 1 | 24 (MyClassCd(MyClassB+8)MyClass)
1>
1> MyClassC::$vftable@MyClass@:
1> | -56
1> 0 | &MyClassC::fun
1>
1> MyClassC::funB this adjustor: 24
1> MyClassC::funC this adjustor: 0
1> MyClassC::fun this adjustor: 56
注意一下MyClassC对象模型,发现从上到下显示虚继承类的内容,按照继承顺序先是MyClassA的内容,然后是MyClassB的内容,最后是公共基类的内容,例外每个虚继承类都带有一个虚基表指针。
虚继承中虚拟公共基类成员的访问:
虚拟继承能避免菱形继承中公共基类成员访问会产生歧义的问题,是怎么做到的呢?我们知道在在非继承、单继承甚至多继承关系中,成员的访问都是通多基类指针和基类成员之间的偏移量来完成的。虚继承中,访问虚基类的成员任然是计算固定偏移量,但是访问公共的虚基类的成员,开销就变得非常大,需要访问两个虚拟指针的内容,具体的步骤如下:
MyClassC* vddc = new MyClassC(); cout << vddc->var << endl;
1.获取一个虚基表指针,这里是第一个vbtr
2.获取虚基表中某一项的内容,这里是40
3.把内容中指出的偏移量加到“虚基类表指针”的地址上,访问公共的虚基类的内容。
虚继承中的向上向下类型转换:
1.虚继承中向上转换和一般继承一样,使用static_cast静态转换
MyClassC* vddc = new MyClassC(); MyClassA* vda = static_cast<MyClassA*>(vddc);
2.虚拟继承中的公共的虚拟基类向下转换是不可以使用静态转换的,因为需要运行时的信息。
MyClass* vd = new MyClass(); MyClassC* vddc = static_cast<MyClassC*>(vd);
error: cannot convert from pointer to base class ‘MyClass’ to pointer to derived class ‘MyClassC’ because the base is virtual
需要使用动态转换,dynamic_cast
MyClass* vd = new MyClass(); MyClassC* vddc = dynamic_cast<MyClassC*>(vd);
通过以上的描述,我们基本认清了C++的对象模型。尤其是在多重、虚拟继承下的复杂结构。通过这些真实的例子,使得我们认清C++内class的本质,以此指导我们更好的书写我们的程序。本文从对象结构的角度结合图例为大家阐述对象的基本模型,和一般描述C++虚拟机制的文章有所不同。作者只希望借助于图表能把C++对象以更好理解的形式为大家展现出来,希望本文对你有所帮助。
参考资料:
1.http://www.cnblogs.com/fanzhidongyzby/archive/2013/01/14/2859064.html
2.http://www.oschina.net/translate/cpp-virtual-inheritance
3.http://www.cnblogs.com/cy568searchx/p/3707384.html
C++ 继承、多继承、虚拟继承对象模型