C/C++知识点之C++ 继承、多继承、虚拟继承对象模型
小标 2019-01-21 来源 : 阅读 1017 评论 0

摘要:本文主要向大家介绍了 C/C++知识点之C++ 继承、多继承、虚拟继承对象模型,通过具体的内容向大家展示,希望对大家学习C/C++知识点有所帮助。

本文主要向大家介绍了 C/C++知识点之C++  继承、多继承、虚拟继承对象模型,通过具体的内容向大家展示,希望对大家学习C/C++知识点有所帮助。

C/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(derivedA);
    derivedA->foo();   //MyClassA:foo()
    derivedA->fun();   //MyClassA:fun()
    base->foo();       //MyClass:foo()
    base->fun();       //MyClassA:fun() 多态

    //向下转换base = new MyClass();
    derivedA = static_cast(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(ddc);
    MyClassB* db = static_cast(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(ddc);
    cout << ddc->var << endl;

 error: ‘MyClass’ is an ambiguous base of ‘MyClassC’
error: request for member ‘var’ is ambiguous
正确的做法要指明来自哪个继承类,避免产生歧义

    MyClass* base = static_cast(static_cast(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 | varA1>     |  (size=4)1>   +---1>   +--- (virtual base MyClass)1>  24 | {vfptr}1>  32 | var1>     |  (size=4)1>   +---1>1>  MyClassA::$vftable@MyClassA@:1>   | &MyClassA_meta1>   |  01>   0 | &MyClassA::funA1>1>  MyClassA::$vbtable@:1>   0 | -81>   1 | 16 (MyClassAd(MyClassA+8)MyClass)1>1>  MyClassA::$vftable@MyClass@:1>   | -241>   0 | &MyClassA::fun1>1>  MyClassA::fun this adjustor: 241>  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 | | varA1>     | |  (size=4)1>   | +---1>   | +--- (base class MyClassB)1>  24 | | {vfptr}1>  32 | | {vbptr}1>  40 | | varB1>     | |  (size=4)1>   | +---1>  48 | varC1>     |  (size=4)1>   +---1>   +--- (virtual base MyClass)1>  56 | {vfptr}1>  64 | var1>     |  (size=4)1>   +---1>1>  MyClassC::$vftable@MyClassA@:1>   | &MyClassC_meta1>   |  01>   0 | &MyClassA::funA1>   1 | &MyClassC::funC1>1>  MyClassC::$vftable@MyClassB@:1>   | -241>   0 | &MyClassC::funB1>1>  MyClassC::$vbtable@MyClassA@:1>   0 | -81>   1 | 48 (MyClassCd(MyClassA+8)MyClass)1>1>  MyClassC::$vbtable@MyClassB@:1>   0 | -81>   1 | 24 (MyClassCd(MyClassB+8)MyClass)1>1>  MyClassC::$vftable@MyClass@:1>   | -561>   0 | &MyClassC::fun1>1>  MyClassC::funB this adjustor: 241>  MyClassC::funC this adjustor: 01>  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(vddc);

 2.虚拟继承中的公共的虚拟基类向下转换是不可以使用静态转换的,因为需要运行时的信息。

    MyClass* vd = new MyClass();
    MyClassC* vddc = static_cast(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(vd);

 通过以上的描述,我们基本认清了C++的对象模型。尤其是在多重、虚拟继承下的复杂结构。通过这些真实的例子,使得我们认清C++内class的本质,以此指导我们更好的书写我们的程序。本文从对象结构的角度结合图例为大家阐述对象的基本模型,和一般描述C++虚拟机制的文章有所不同。作者只希望借助于图表能把C++对象以更好理解的形式为大家展现出来,希望本文对你有所帮助。

本文由职坐标整理并发布,希望对同学们有所帮助。了解更多详情请关注职坐标编程语言C/C+频道!

本文由 @小标 发布于职坐标。未经许可,禁止转载。
喜欢 | 0 不喜欢 | 0
看完这篇文章有何感觉?已经有0人表态,0%的人喜欢 快给朋友分享吧~
评论(0)
后参与评论

您输入的评论内容中包含违禁敏感词

我知道了

助您圆梦职场 匹配合适岗位
验证码手机号,获得海同独家IT培训资料
选择就业方向:
人工智能物联网
大数据开发/分析
人工智能Python
Java全栈开发
WEB前端+H5

请输入正确的手机号码

请输入正确的验证码

获取验证码

您今天的短信下发次数太多了,明天再试试吧!

提交

我们会在第一时间安排职业规划师联系您!

您也可以联系我们的职业规划师咨询:

小职老师的微信号:z_zhizuobiao
小职老师的微信号:z_zhizuobiao

版权所有 职坐标-一站式IT培训就业服务领导者 沪ICP备13042190号-4
上海海同信息科技有限公司 Copyright ©2015 www.zhizuobiao.com,All Rights Reserved.
 沪公网安备 31011502005948号    

©2015 www.zhizuobiao.com All Rights Reserved

208小时内训课程