首页 > 代码库 > 虚函数与虚继承
虚函数与虚继承
寻踪浅析
封装、继承、多态是面向对象语言的三大特性,熟悉C++的人对此应该不会有太多异议。C语言提供的struct,顶多算得上对数据的简单封装,而C++的引入把struct“升级”为class,使得面向对象的概念更加强大。继承机制解决了对象复用的问题,然而多重继承又会产生成员冲突的问题,虚继承在我看来更像是一种“不得已”的解决方案。多态让对象具有了运行时特性,并且它是软件设计复用的本质,虚函数的出现为多态性质提供了实现手段。
如果说C语言的struct相当于对数据成员简单的排列(可能有对齐问题),那么C++的class让对象的数据的封装变得更加复杂。所有的这些问题来源于C++的一个关键字——virtual!virtual在C++中最大的功能就是声明虚函数和虚基类,有了这种机制,C++对象的机制究竟发生了怎样的变化,让我们一起探寻之。
为了查看对象的结构模型,我们需要在编译器配置时做一些初始化。在VS2010中,在项目——属性——配置属性——C/C++——命令行——其他选项中添加选项“/d1reportAllClassLayout”。再次编译时候,编译器会输出所有定义类的对象模型。由于输出的信息过多,我们可以使用“Ctrl+F”查找命令,找到对象模型的输出。
一、基本对象模型
首先,我们定义一个简单的类,它含有一个数据成员和一个虚函数。
{
int var;
public:
virtual void fun()
{}
};
编译输出的MyClass对象结构如下:
1> +---
1> 0 | {vfptr}
1> 4 | var
1> +---
1>
1> MyClass::$vftable@:
1> | &MyClass_meta
1> | 0
1> 0 | &MyClass::fun
1>
1> MyClass::fun this adjustor: 0
从这段信息中我们看出,MyClass对象大小是8个字节。前四个字节存储的是虚函数表的指针vfptr,后四个字节存储对象成员var的值。虚函数表的大小为4字节,就一条函数地址,即虚函数fun的地址,它在虚函数表vftable的偏移是0。因此,MyClass对象模型的结果如图1所示。
图1 MyClass对象模型
MyClass的虚函数表虽然只有一条函数记录,但是它的结尾处是由4字节的0作为结束标记的。
adjust表示虚函数机制执行时,this指针的调整量,假如fun被多态调用的话,那么它的形式如下:
*(this+0)[0]()
总结虚函数调用形式,应该是:
*(this指针+调整量)[虚函数在vftable内的偏移]()
二、单重继承对象模型
我们定义一个继承于MyClass类的子类MyClassA,它重写了fun函数,并且提供了一个新的虚函数funA。
{
int varA;
public:
virtual void fun()
{}
virtual void funA()
{}
};
它的对象模型为:
1> +---
1> | +--- (base class MyClass)
1> 0 | | {vfptr}
1> 4 | | var
1> | +---
1> 8 | varA
1> +---
1>
1> MyClassA::$vftable@:
1> | &MyClassA_meta
1> | 0
1> 0 | &MyClassA::fun
1> 1 | &MyClassA::funA
1>
1> MyClassA::fun this adjustor: 0
1> MyClassA::funA this adjustor: 0
可以看出,MyClassA将基类MyClass完全包含在自己内部,包括vfptr和var。并且虚函数表内的记录多了一条——MyClassA自己定义的虚函数funA。它的对象模型如图2所示。
图2 MyClassA对象模型
我们可以得出结论:在单继承形式下,子类的完全获得父类的虚函数表和数据。子类如果重写了父类的虚函数(如fun),就会把虚函数表原本fun对应的记录(内容MyClass::fun)覆盖为新的函数地址(内容MyClassA::fun),否则继续保持原本的函数地址记录。如果子类定义了新的虚函数,虚函数表内会追加一条记录,记录该函数的地址(如MyClassA::funA)。
使用这种方式,就可以实现多态的特性。假设我们使用如下语句:
pc->fun();
编译器在处理第二条语句时,发现这是一个多态的调用,那么就会按照上边我们对虚函数的多态访问机制调用函数fun。
*(pc+0)[0]()
因为虚函数表内的函数地址已经被子类重写的fun函数地址覆盖了,因此该处调用的函数正是MyClassA::fun,而不是基类的MyClass::fun。
如果使用MyClassA对象直接访问fun,则不会出发多态机制,因为这个函数调用在编译时期是可以确定的,编译器只需要直接调用MyClassA::fun即可。
三、多重继承对象模型
和前边MyClassA类似,我们也定义一个类MyClassB。
{
int varB;
public:
virtual void fun()
{}
virtual void funB()
{}
};
它的对象模型和MyClassA完全类似,这里就不再赘述了。
为了实现多重继承,我们再定义一个类MyClassC。
{
int varC;
public:
virtual void funB()
{}
virtual void funC()
{}
};
为了简化,我们让MyClassC只重写父类MyClassB的虚函数funB,它的对象模型如下:
1> +---
1> | +--- (base class MyClassA)
1> | | +--- (base class MyClass)
1> 0 | | | {vfptr}
1> 4 | | | var
1> | | +---
1> 8 | | varA
1> | +---
1> | +--- (base class MyClassB)
1> | | +--- (base class MyClass)
1> 12 | | | {vfptr}
1> 16 | | | var
1> | | +---
1> 20 | | varB
1> | +---
1> 24 | varC
1> +---
1>
1> MyClassC::$vftable@MyClassA@:
1> | &MyClassC_meta
1> | 0
1> 0 | &MyClassA::fun
1> 1 | &MyClassA::funA
1> 2 | &MyClassC::funC
1>
1> MyClassC::$vftable@MyClassB@:
1> | -12
1> 0 | &MyClassB::fun
1> 1 | &MyClassC::funB
1>
1> MyClassC::funB this adjustor: 12
1> MyClassC::funC this adjustor: 0
和单重继承类似,多重继承时MyClassC会把所有的父类全部按序包含在自身内部。而且每一个父类都对应一个单独的虚函数表。MyClassC的对象模型如图3所示。
图3 MyClassC对象模型
多重继承下,子类不再具有自身的虚函数表,它的虚函数表与第一个父类的虚函数表合并了。同样的,如果子类重写了任意父类的虚函数,都会覆盖对应的函数地址记录。如果MyClassC重写了fun函数(两个父类都有该函数),那么两个虚函数表的记录都需要被覆盖!在这里我们发现MyClassC::funB的函数对应的adjust值是12,按照我们前边的规则,可以发现该函数的多态调用形式为:
*(this+12)[1]()
此处的调整量12正好是MyClassB的vfptr在MyClassC对象内的偏移量。
四、虚拟继承对象模型
虚拟继承是为了解决多重继承下公共基类的多份拷贝问题。比如上边的例子中MyClassC的对象内包含MyClassA和MyClassB子对象,但是MyClassA和MyClassB内含有共同的基类MyClass。为了消除MyClass子对象的多份存在,我们需要让MyClassA和MyClassB都虚拟继承于MyClass,然后再让MyClassC多重继承于这两个父类。相对于上边的例子,类内的设计不做任何改动,先修改MyClassA和MyClassB的继承方式:
class MyClassB:virtual public MyClass
class MyClassC:public MyClassA,public MyClassB
由于虚继承的本身语义,MyClassC内必须重写fun函数,因此我们需要再重写fun函数。这种情况下,MyClassC的对象模型如下:
1> +---
1> | +--- (base class MyClassA)
1> 0 | | {vfptr}
1> 4 | | {vbptr}
1> 8 | | varA
1> | +---
1> | +--- (base class MyClassB)
1> 12 | | {vfptr}
1> 16 | | {vbptr}
1> 20 | | varB
1> | +---
1> 24 | varC
1> +---
1> +--- (virtual base MyClass)
1> 28 | {vfptr}
1> 32 | var
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> | -12
1> 0 | &MyClassC::funB
1>
1> MyClassC::$vbtable@MyClassA@:
1> 0 | -4
1> 1 | 24 (MyClassCd(MyClassA+4)MyClass)
1>
1> MyClassC::$vbtable@MyClassB@:
1> 0 | -4
1> 1 | 12 (MyClassCd(MyClassB+4)MyClass)
1>
1> MyClassC::$vftable@MyClass@:
1> | -28
1> 0 | &MyClassC::fun
1>
1> MyClassC::fun this adjustor: 28
1> MyClassC::funB this adjustor: 12
1> MyClassC::funC this adjustor: 0
1>
1> vbi: class offset o.vbptr o.vbte fVtorDisp
1> MyClass 28 4 4 0
虚继承的引入把对象的模型变得十分复杂,除了每个基类(MyClassA和MyClassB)和公共基类(MyClass)的虚函数表指针需要记录外,每个虚拟继承了MyClass的父类还需要记录一个虚基类表vbtable的指针vbptr。MyClassC的对象模型如图4所示。
图4 MyClassC对象模型
虚基类表每项记录了被继承的虚基类子对象相对于虚基类表指针的偏移量。比如MyClassA的虚基类表第二项记录值为24,正是MyClass::vfptr相对于MyClassA::vbptr的偏移量,同理MyClassB的虚基类表第二项记录值12也正是MyClass::vfptr相对于MyClassA::vbptr的偏移量。
和虚函数表不同的是,虚基类表的第一项记录着当前子对象相对与虚基类表指针的偏移。MyClassA和MyClassB子对象内的虚表指针都是存储在相对于自身的4字节偏移处,因此该值是-4。假定MyClassA和MyClassC或者MyClassB内没有定义新的虚函数,即不会产生虚函数表,那么虚基类表第一项字段的值应该是0。
通过以上的对象组织形式,编译器解决了公共虚基类的多份拷贝的问题。通过每个父类的虚基类表指针,都能找到被公共使用的虚基类的子对象的位置,并依次访问虚基类子对象的数据。至于虚基类定义的虚函数,它和其他的虚函数的访问形式相同,本例中,如果使用虚基类指针MyClass*pc访问MyClassC对象的fun,将会被转化为如下形式:
*(pc+28)[0]()
编译器实现:
首先,说说GCC的编译器.
它实现比较简单,不管是否虚继承,GCC都是将虚表指针在整个继承关系中共享的,不共享的是指向虚基类的指针。
class A {
int a;
virtual ~A(){}
};
class B:virtual public A{
virtual void myfunB(){}
};
class C:virtual public A{
virtual void myfunC(){}
};
class D:public B,public C{
virtual void myfunD(){}
};
以上代码中sizeof(A)=8,sizeof(B)=12,sizeof(C)=12,sizeof(D)=16.
解释:A中int+虚表指针。B,C中由于是虚继承因此大小为A+指向虚基类的指针,B,C虽然加入了自己的虚函数,但是虚表指针是和基类共享的,因此不会有自己的虚表指针。D由于B,C都是虚继承,因此D只包含一个A的副本,于是D大小就等于A+B中的指向虚基类的指针+C中的指向虚基类的指针。
如果B,C不是虚继承,而是普通继承的话,那么A,B,C的大小都是8(没有指向虚基类的指针了),而D由于不是虚继承,因此包含两个A副本,大小为16.注意此时虽然D的大小和虚继承一样,但是内存布局却不同。
然后,来看看VC的编译器
vc对虚表指针的处理比GCC复杂,它根据是否为虚继承来判断是否在继承关系中共享虚表指针,而对指向虚基类的指针和GCC一样是不共享,当然也不可能共享。
代码同上。
运行结果将会是sizeof(A)=8,sizeof(B)=16,sizeof(C)=16,sizeof(D)=24.
解释:A中依然是int+虚表指针。B,C中由于是虚继承因此虚表指针不共享,由于B,C加入了自己的虚函数,所以B,C分别自己维护一个虚表指针,它指向自己的虚函数。(注意:只有子类有新的虚函数时,编译器才会在子类中添加虚表指针)因此B,C大小为A+自己的虚表指针+指向虚基类的指针。D由于B,C都是虚继承,因此D只包含一个A的副本,同时D是从B,C普通继承的,而不是虚继承的,因此没有自己的虚表指针。于是D大小就等于A+B的虚表指针+C的虚表指针+B中的指向虚基类的指针+C中的指向虚基类的指针。
同样,如果去掉虚继承,结果将和GCC结果一样,A,B,C都是8,D为16,原因就是VC的编译器对于非虚继承,父类和子类是共享虚表指针的。
利用visual studio 命令提示(2008),到xx.cpp 文件目录下 运行cl /d1 reportSingleClassLayoutB xx.cpp
第一个vfptr 指向B的虚表,第二个vbptr指向A,第三个指向A的虚表,因为是虚拟继承,所以子类中有一个指向父类的虚基类指针,
防止菱形继承中数据重复,这样在菱形继承中,不会出现祖先数据重复,而只指向祖先数据的指针
虚函数表:http://blog.csdn.net/haoel/article/details/1948051/
参考:
http://blog.csdn.net/gxiaob/article/details/10149069
http://www.cnblogs.com/fanzhidongyzby/archive/2013/01/14/2859064.html