虚函数继承与虚继承

来源:互联网 发布:ubuntu搭建lamp 编辑:程序博客网 时间:2024/05/21 11:08

一、虚函数继承(原文转自http://blog.chinaunix.net/uid-25132162-id-1564955.html)

1、空类,空类单继承,空类多继承的sizeof

#include <iostream>using namespace std;class Base1{};class Base2{};class Derived1:public Base1{};class Derived2:public Base1, public Base2{};int main() {     Base1 b1;    Base2 b2;    Derived1 d1;    Derived2 d2;    cout<<"sizeof(Base1) = "<<sizeof(Base1)<<" sizeof(b1) = "<<sizeof(b1)<<endl;     cout<<"sizeof(Base2) = "<<sizeof(Base2)<<" sizeof(b2) = "<<sizeof(b2)<<endl;    cout<<"sizeof(Derived1) = "<<sizeof(Derived1)<<" sizeof(d1) = "<<sizeof(d1)<<endl;    cout<<"sizeof(Derived2) = "<<sizeof(Derived2)<<" sizeof(d1) = "<<sizeof(d1)<<endl;      return 0; }
结果为:
sizeof(Base1) = 1 sizeof(b1) = 1
sizeof(Base2) = 1 sizeof(b2) = 1
sizeof(Derived1) = 1 sizeof(d1) = 1
sizeof(Derived2) = 1 sizeof(d1) = 1
可以看出所有的结果都是1。

2、含有虚函数的类以及虚继承类的sizeof
虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。编译器必需要保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证正确取到虚函数的偏移量)。
假设我们有这样的一个类:
class Base {
public:
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
virtual void h() { cout << "Base::h" << endl; }
};
当我们定义一个这个类的实例,Base b时,其b中成员的存放如下:

指向虚函数表的指针在对象b的最前面。虚函数表的最后多加了一个结点,这是虚函数表的结束结点,就像字符串的结束符“\0”一样,其标志了虚函数表的结束。这个结束标志的值在不同的编译器下是不同的。在WinXP+VS2003下,这个值是NULL。而在Ubuntu 7.10 + Linux 2.6.22 + GCC 4.1.3下,这个值是如果1,表示还有下一个虚函数表,如果值是0,表示是最后一个虚函数表。
因为对象b中多了一个指向虚函数表的指针,而指针的sizeof是4,因此含有虚函数的类或实例最后的sizeof是实际的数据成员的sizeof加4。
下面将讨论针对基类含有虚函数的继承讨论
(1)在派生类中不对基类的虚函数进行覆盖,同时派生类中还拥有自己的虚函数,比如有如下的派生类:

class Derived: public Base {public:virtual void f1() { cout << "Derived::f1" << endl; }virtual void g1() { cout << "Derived::g1" << endl; }virtual void h1() { cout << "Derived::h1" << endl; }};
基类和派生类的关系如下:


当定义一个Derived的对象d后,其成员的存放如下:


可以发现:
1)虚函数按照其声明顺序放于表中。
2)父类的虚函数在子类的虚函数前面。
此时基类和派生类的sizeof都是数据成员的sizeof加4。
(2)在派生类中对基类的虚函数进行覆盖,假设有如下的派生类:

class Derived: public Base {public:virtual void f() { cout << "Derived::f" << endl; }virtual void g1() { cout << "Derived::g1" << endl; }virtual void h1() { cout << "Derived::h1" << endl; }};
基类和派生类之间的关系:其中基类的虚函数f在派生类中被覆盖了

当我们定义一个派生类对象d后,其d的成员存放为:

可以发现:
1)覆盖的f()函数被放到了虚表中原来父类虚函数的位置。
2)没有被覆盖的函数依旧。
这样,我们就可以看到对于下面这样的程序,
Base *b = new Derive();
b->f();
由b所指的内存中的虚函数表的f()的位置已经被Derive::f()函数地址所取代,于是在实际调用发生时,是Derive::f()被调用了。这就实现了多态。
(3)多继承:无虚函数覆盖
假设基类和派生类之间有如下关系:

对于子类实例中的虚函数表,是下面这个样子:

我们可以看到:
1) 每个父类都有自己的虚表。
2) 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)
由于每个基类都需要一个指针来指向其虚函数表,因此d的sizeof等于d的数据成员加3*4=12。
(4)多重继承,含虚函数覆盖
假设,基类和派生类又如下关系:派生类中覆盖了基类的虚函数f

下面是对于子类实例中的虚函数表的图:

我们可以看见,三个父类虚函数表中的f()的位置被替换成了子类的函数指针。这样,我们就可以任一静态类型的父类来指向子类,并调用子类的f()了。如:

Derive d;Base1 *b1 = &d;Base2 *b2 = &d;Base3 *b3 = &d;b1->f(); //Derive::f()b2->f(); //Derive::f()b3->f(); //Derive::f()b1->g(); //Base1::g()b2->g(); //Base2::g()b3->g(); //Base3::g()
二、虚继承(原文转自http://blog.csdn.net/gxiaob/article/details/10149069)

虚拟继承是为了解决多重继承下公共基类的多份拷贝问题。

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)=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中的指向虚基类的指针。
如果去掉虚继承,,A,B,C都是8,D为16,原因就是VC的编译器对于非虚继承,父类和子类是共享虚表指针的。

#include <iostream>using namespace std;class Base{public:    virtual void f();    virtual void g();    virtual void h();};class Derived1: public Base{public:    virtual void f1();    virtual void g1();    virtual void h1();};class Derived2:public Base{public:    virtual void f();    virtual void g1();    virtual void h1();};class Derived3:virtual public Base{public:    virtual void f1();    virtual void g1();    virtual void h1();};class Derived4:virtual public Base{public:    virtual void f();    virtual void g1();    virtual void h1();};class Derived5:virtual public Base{public:    virtual void f();    virtual void g();    virtual void h();};class Derived6:virtual public Base{};int main() {     cout<<sizeof(Base)<<endl; //4    cout<<sizeof(Derived1)<<endl; //4    cout<<sizeof(Derived2)<<endl; //4    cout<<sizeof(Derived3)<<endl; //12    cout<<sizeof(Derived4)<<endl; //12    cout<<sizeof(Derived5)<<endl; //8    cout<<sizeof(Derived6)<<endl; //8    return 0; }
对于Base, Derived1和Derived2的结果根据前面关于继承的分析是比较好理解的,不过对于虚继承的方式则有点不一样了,根据结果自己得出的一种关于虚继承的分析,如对Derived3或Derived4定义一个对象d,其里面会出现三个跟虚函数以及虚继承的指针,因为是虚继承,因此引入一个指针指向虚继承的基类,第二由于在基类中有虚函数,因此需要指针指向其虚函数表,由于派生类自己本身也有自己的虚函数,因为采取的是虚继承,因此它自己的虚函数不会放到基类的虚函数表的后面,而是另外分配一个只存放自己的虚函数的虚函数表,于是又引入一个指针,从例子中看到Derived5和Derived6的结果是8,原因是在派生类要么没有自己的虚函数,要么全部都是对基类虚函数的覆盖,因此就少了指向其派生类自己的虚函数表的指针,故结果要少4。
三、虚函数与虚继承寻踪(原文转自http://www.cnblogs.com/fanzhidongyzby/archive/2013/01/14/2859064.html)

封装、继承、多态是面向对象语言的三大特性,熟悉C++的人对此应该不会有太多异议。C语言提供的struct,顶多算得上对数据的简单封装,而C++的引入把struct“升级”为class,使得面向对象的概念更加强大。继承机制解决了对象复用的问题,然而多重继承又会产生成员冲突的问题,虚继承在我看来更像是一种“不得已”的解决方案。多态让对象具有了运行时特性,并且它是软件设计复用的本质,虚函数的出现为多态性质提供了实现手段。
如果说C语言的struct相当于对数据成员简单的排列(可能有对齐问题),那么C++的class让对象的数据的封装变得更加复杂。所有的这些问题来源于C++的一个关键字——virtual!virtual在C++中最大的功能就是声明虚函数和虚基类,有了这种机制,C++对象的机制究竟发生了怎样的变化,让我们一起探寻之。
为了查看对象的结构模型,我们需要在编译器配置时做一些初始化。在VS2010中,在项目——属性——配置属性——C/C++——命令行——其他选项中添加选项“/d1reportAllClassLayout”。再次编译时候,编译器会输出所有定义类的对象模型。由于输出的信息过多,我们可以使用“Ctrl+F”查找命令,找到对象模型的输出。

1、基本对象模型
首先,我们定义一个简单的类,它含有一个数据成员和一个虚函数。

class MyClass{    int var;public:    virtual void fun()    {}};
编译输出的MyClass对象结构如下:

1>  class MyClass    size(8):1>      +---1>   0    | {vfptr}1>   4    | var1>      +---1>  1>  MyClass::$vftable@:1>      | &MyClass_meta1>      |  01>   0    | &MyClass::fun1>  1>  MyClass::fun this adjustor: 0
从这段信息中我们看出,MyClass对象大小是8个字节。前四个字节存储的是虚函数表的指针vfptr,后四个字节存储对象成员var的值。虚函数表的大小为4字节,就一条函数地址,即虚函数fun的地址,它在虚函数表vftable的偏移是0。因此,MyClass对象模型的结果如图所示。


MyClass的虚函数表虽然只有一条函数记录,但是它的结尾处是由4字节的0作为结束标记的。
adjust表示虚函数机制执行时,this指针的调整量,假如fun被多态调用的话,那么它的形式如下:
*(this+0)[0]()
总结虚函数调用形式,应该是:
*(this指针+调整量)[虚函数在vftable内的偏移]()

2、单重继承对象模型
我们定义一个继承于MyClass类的子类MyClassA,它重写了fun函数,并且提供了一个新的虚函数funA。

class MyClassA:public MyClass{    int varA;public:    virtual void fun()    {}    virtual void funA()    {}};
它的对象模型为:

1>  class MyClassA    size(12):1>      +---1>      | +--- (base class MyClass)1>   0    | | {vfptr}1>   4    | | var1>      | +---1>   8    | varA1>      +---1>  1>  MyClassA::$vftable@:1>      | &MyClassA_meta1>      |  01>   0    | &MyClassA::fun1>   1    | &MyClassA::funA1>  1>  MyClassA::fun this adjustor: 01>  MyClassA::funA this adjustor: 0
可以看出,MyClassA将基类MyClass完全包含在自己内部,包括vfptr和var。并且虚函数表内的记录多了一条——MyClassA自己定义的虚函数funA。它的对象模型如下图所示。

我们可以得出结论:在单继承形式下,子类的完全获得父类的虚函数表和数据。子类如果重写了父类的虚函数(如fun),就会把虚函数表原本fun对应的记录(内容MyClass::fun)覆盖为新的函数地址(内容MyClassA::fun),否则继续保持原本的函数地址记录。如果子类定义了新的虚函数,虚函数表内会追加一条记录,记录该函数的地址(如MyClassA::funA)。
使用这种方式,就可以实现多态的特性。假设我们使用如下语句:

MyClass*pc=new MyClassA;pc->fun();
编译器在处理第二条语句时,发现这是一个多态的调用,那么就会按照上边我们对虚函数的多态访问机制调用函数fun。
*(pc+0)[0]()
因为虚函数表内的函数地址已经被子类重写的fun函数地址覆盖了,因此该处调用的函数正是MyClassA::fun,而不是基类的MyClass::fun。
如果使用MyClassA对象直接访问fun,则不会出发多态机制,因为这个函数调用在编译时期是可以确定的,编译器只需要直接调用MyClassA::fun即可。

3、多重继承对象模型
和前边MyClassA类似,我们也定义一个类MyClassB。

class MyClassB:public MyClass{    int varB;public:    virtual void fun()    {}    virtual void funB()    {}};
它的对象模型和MyClassA完全类似,这里就不再赘述了。
为了实现多重继承,我们再定义一个类MyClassC。
class MyClassC:public MyClassA,public MyClassB{    int varC;public:    virtual void funB()    {}virtual void funC()    {}};
为了简化,我们让MyClassC只重写父类MyClassB的虚函数funB,它的对象模型如下:
1>  class MyClassC    size(28):1>      +---1>      | +--- (base class MyClassA)1>      | | +--- (base class MyClass)1>   0    | | | {vfptr}1>   4    | | | var1>      | | +---1>   8    | | varA1>      | +---1>      | +--- (base class MyClassB)1>      | | +--- (base class MyClass)1>  12    | | | {vfptr}1>  16    | | | var1>      | | +---1>  20    | | varB1>      | +---1>  24    | varC1>      +---1>  1>  MyClassC::$vftable@MyClassA@:1>      | &MyClassC_meta1>      |  01>   0    | &MyClassA::fun1>   1    | &MyClassA::funA1>   2    | &MyClassC::funC1>  1>  MyClassC::$vftable@MyClassB@:1>      | -121>   0    | &MyClassB::fun1>   1    | &MyClassC::funB1>  1>  MyClassC::funB this adjustor: 121>  MyClassC::funC this adjustor: 0
和单重继承类似,多重继承时MyClassC会把所有的父类全部按序包含在自身内部。而且每一个父类都对应一个单独的虚函数表。MyClassC的对象模型如下图所示。

多重继承下,子类不再具有自身的虚函数表,它的虚函数表与第一个父类的虚函数表合并了。同样的,如果子类重写了任意父类的虚函数,都会覆盖对应的函数地址记录。如果MyClassC重写了fun函数(两个父类都有该函数),那么两个虚函数表的记录都需要被覆盖!在这里我们发现MyClassC::funB的函数对应的adjust值是12,按照我们前边的规则,可以发现该函数的多态调用形式为:
*(this+12)[1]()
此处的调整量12正好是MyClassB的vfptr在MyClassC对象内的偏移量。

4、虚拟继承对象模型
虚拟继承是为了解决多重继承下公共基类的多份拷贝问题。比如上边的例子中MyClassC的对象内包含MyClassA和MyClassB子对象,但是MyClassA和MyClassB内含有共同的基类MyClass。为了消除MyClass子对象的多份存在,我们需要让MyClassA和MyClassB都虚拟继承于MyClass,然后再让MyClassC多重继承于这两个父类。相对于上边的例子,类内的设计不做任何改动,先修改MyClassA和MyClassB的继承方式:

class MyClassA:virtual public MyClassclass MyClassB:virtual public MyClassclass MyClassC:public MyClassA,public MyClassB
由于虚继承的本身语义,MyClassC内必须重写fun函数,因此我们需要再重写fun函数。这种情况下,MyClassC的对象模型如下

1>  class MyClassC    size(36):1>      +---1>      | +--- (base class MyClassA)1>   0    | | {vfptr}1>   4    | | {vbptr}1>   8    | | varA1>      | +---1>      | +--- (base class MyClassB)1>  12    | | {vfptr}1>  16    | | {vbptr}1>  20    | | varB1>      | +---1>  24    | varC1>      +---1>      +--- (virtual base MyClass)1>  28    | {vfptr}1>  32    | var1>      +---1>  1>  MyClassC::$vftable@MyClassA@:1>      | &MyClassC_meta1>      |  01>   0    | &MyClassA::funA1>   1    | &MyClassC::funC1>  1>  MyClassC::$vftable@MyClassB@:1>      | -121>   0    | &MyClassC::funB1>  1>  MyClassC::$vbtable@MyClassA@:1>   0    | -41>   1    | 24 (MyClassCd(MyClassA+4)MyClass)1>  1>  MyClassC::$vbtable@MyClassB@:1>   0    | -41>   1    | 12 (MyClassCd(MyClassB+4)MyClass)1>  1>  MyClassC::$vftable@MyClass@:1>      | -281>   0    | &MyClassC::fun1>  1>  MyClassC::fun this adjustor: 281>  MyClassC::funB this adjustor: 121>  MyClassC::funC this adjustor: 01>  1>  vbi:       class  offset o.vbptr  o.vbte fVtorDisp1>           MyClass      28       4       4 0
虚继承的引入把对象的模型变得十分复杂,除了每个基类(MyClassA和MyClassB)和公共基类(MyClass)的虚函数表指针需要记录外,每个虚拟继承了MyClass的父类还需要记录一个虚基类表vbtable的指针vbptr。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]()

通过以上的描述,我们基本认清了C++的对象模型。尤其是在多重、虚拟继承下的复杂结构。通过这些真实的例子,使得我们认清C++内class的本质,以此指导我们更好的书写我们的程序。本文从对象结构的角度结合图例为大家阐述对象的基本模型,和一般描述C++虚拟机制的文章有所不同。























0 0
原创粉丝点击