首页 > 代码库 > 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++ 继承、多继承、虚拟继承对象模型