图文例解C类的多重继承与虚拟继承.docx_第1页
图文例解C类的多重继承与虚拟继承.docx_第2页
图文例解C类的多重继承与虚拟继承.docx_第3页
图文例解C类的多重继承与虚拟继承.docx_第4页
图文例解C类的多重继承与虚拟继承.docx_第5页
已阅读5页,还剩13页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

图文例解C+类的多重继承与虚拟继承在过去的学习中,我们始终接触的单个类的继承,但是在现实生活中,一些新事物往往会拥有两个或者两个以上事物的属性,为了解决这个问题,C+引入了多重继承的概念,C+允许为一个派生类指定多个基类,这样的继承结构被称做多重继承。举个例子,交通工具类可以派生出汽车和船连个子类,但拥有汽车和船共同特性水陆两用汽车就必须继承来自汽车类与船类的共同属性。由此我们不难想出如下的图例与代码:当一个派生类要使用多重继承的时候,必须在派生类名和冒号之后列出所有基类的类名,并用逗好分隔。/程序作者:管宁/站点:/所有稿件均有版权,如要转载,请务必著名出处和作者#includeusingnamespacestd;classVehiclepublic:Vehicle(intweight=0)Vehicle:weight=weight;voidSetWeight(intweight)cout重新设置重量endl;Vehicle:weight=weight;virtualvoidShowMe()=0;protected:intweight;classCar:publicVehicle/汽车public:Car(intweight=0,intaird=0):Vehicle(weight)Car:aird=aird;voidShowMe()cout我是汽车!endl;protected:intaird;classBoat:publicVehicle/船public:Boat(intweight=0,floattonnage=0):Vehicle(weight)Boat:tonnage=tonnage;voidShowMe()cout我是船!endl;protected:floattonnage;classAmphibianCar:publicCar,publicBoat/水陆两用汽车,多重继承的体现public:AmphibianCar(intweight,intaird,floattonnage):Vehicle(weight),Car(weight,aird),Boat(weight,tonnage)/多重继承要注意调用基类构造函数voidShowMe()cout我是水陆两用汽车!endl;intmain()AmphibianCara(4,200,1.35f);/错误a.SetWeight(3);/错误system(pause);上面的代码从表面看,看不出有明显的语发错误,但是它是不能够通过编译的。这有是为什么呢?这是由于多重继承带来的继承的模糊性带来的问题。先看如下的图示:在图中深红色标记出来的地方正是主要问题所在,水陆两用汽车类继承了来自Car类与Boat类的属性与方法,Car类与Boat类同为AmphibianCar类的基类,在内存分配上AmphibianCar获得了来自两个类的SetWeight()成员函数,当我们调用a.SetWeight(3)的时候计算机不知道如何选择分别属于两个基类的被重复拥有了的类成员函数SetWeight()。由于这种模糊问题的存在同样也导致了AmphibianCar a(4,200,1.35f);执行失败,系统会产生Vehicle”不是基或成员的错误。以上面的代码为例,我们要想让AmphibianCar类既获得一个Vehicle的拷贝,而且又同时共享用Car类与Boat类的数据成员与成员函数就必须通过C+所提供的虚拟继承技术来实现。我们在Car类和Boat类继承Vehicle类出,在前面加上virtual关键字就可以实现虚拟继承,使用虚拟继承后,当系统碰到多重继承的时候就会自动先加入一个Vehicle的拷贝,当再次请求一个Vehicle的拷贝的时候就会被忽略,保证继承类成员函数的唯一性。修改后的代码如下,注意观察变化:/程序作者:管宁/站点:/所有稿件均有版权,如要转载,请务必著名出处和作者#includeusingnamespacestd;classVehiclepublic:Vehicle(intweight=0)Vehicle:weight=weight;cout载入Vehicle类构造函数endl;voidSetWeight(intweight)cout重新设置重量endl;Vehicle:weight=weight;virtualvoidShowMe()=0;protected:intweight;classCar:virtualpublicVehicle/汽车,这里是虚拟继承public:Car(intweight=0,intaird=0):Vehicle(weight)Car:aird=aird;cout载入Car类构造函数endl;voidShowMe()cout我是汽车!endl;protected:intaird;classBoat:virtualpublicVehicle/船,这里是虚拟继承public:Boat(intweight=0,floattonnage=0):Vehicle(weight)Boat:tonnage=tonnage;cout载入Boat类构造函数endl;voidShowMe()cout我是船!endl;protected:floattonnage;classAmphibianCar:publicCar,publicBoat/水陆两用汽车,多重继承的体现public:AmphibianCar(intweight,intaird,floattonnage):Vehicle(weight),Car(weight,aird),Boat(weight,tonnage)/多重继承要注意调用基类构造函数cout载入AmphibianCar类构造函数endl;voidShowMe()cout我是水陆两用汽车!endl;voidShowMembers()cout重量:weight顿,空气排量:airdCC,排水量:tonnage顿endl;intmain()AmphibianCara(4,200,1.35f);a.ShowMe();a.ShowMembers();a.SetWeight(3);a.ShowMembers();system(pause);注意观察类构造函数的构造顺序。虽然说虚拟继承与虚函数有一定相似的地方,但读者务必要记住,他们之间是绝对没有任何联系的!如何正确使用C+多重继承2011年06月17日 Asp J原创文章,转载请注明:转载自Soul Apogee本文链接地址:如何正确使用C+多重继承C+多重继承一直是一个让人搞不太清楚的一个问题,但是有时候为了实现多个接口,多重继承是基本不可避免,当然在Windows下我们有强大的COM来帮我们搞定这个事情,不过如果你想自己实现一套类似于COM的东西出来的时候,麻烦事情就来了。在COM里面,有两个很基础的,而且我们都会用到的特性:1. 纯虚接口:一般使用一个只有纯虚函数的类来作为接口2. 引用计数:在C+中一般都使用引用计数来管理对象的生命周期这两个功能在一般设计C+接口的时候也经常用到。其实说到底,上面这两个特性牵扯到的是多重继承的二个表现:1. 多重继承中的数据存储2. 多重继承中的虚函数在COM中,纯虚接口是使用的interface来定义的,引用计数是通过IUnknown接口来实现的,所有的接口都是从IUnknown这种接口中派生出来的。当我们需要某一个类实现多个接口的时候,就经常会遇见这样的多重继承:multi-inheritance-com:哦?!是不是很眼熟,ios,istream,ostream,iostream。各种C+书籍最喜欢用的一个示例。好吧,现在我们先自己实现一个吧,看看到底要怎么使用多重继承。多重继承中对象的的数据存储?1234567891011121314151617181920212223242526272829303132333435363738394041424344454647#include class IBasepublic:IBase() : n(0) virtual IBase() void show() printf(%dn, n); int inc() return +n; int dec() return -n; protected:int n;class IA : public IBasepublic:virtual IA() ;class IB : public IBasepublic:virtual IB() ;class CImpl : public IA, public IBpublic:virtual CImpl() ;int main(int argc, char* argv)CImpl o;IA *pA = &o;IB *pB = &o;pA-inc();pA-show();pB-dec();pB-show();return 0;编译,OK,成功了!好,运行试一试。run-result-1:为什么是1和-1呢?明明n只在继承的一个类IBase里面有,一次加1,一次减一,结果不是应该是1和0么?是不是很奇怪?这便是使用多重继承的时候经常产生的第一个问题:多副本的数据存储。当然这个问题很好解决,只需要使用虚继承即可解决。只需要在IA和IB的定义中,在public IBase前加入virtual关键字即可。?12class IA : virtual public IBaseclass IB : virtual public IBase现在再让我们来看一看运行结果:run-result-2:结果已经正确了,为什么会发生这种情况呢?虚继承到底干了些什么呢?我们先来看看在没有使用虚继承的情况下,CImpl的在内存中是怎么样的:cimpl-memory-normal:对于普通的public继承(非虚继承),C+会将基类和派生类的内容放在一块,合起来组成一个完整的派生类,在内存上看,它们是连在一起的。按照这样的规则,在这里,IA和IB中就会各包含一个IBase,而IA,IB和CImpl中的部分内容又共同组成了CImpl。在将CImpl对象o的指针转型成IA的指针pA过程中,指针将被移动到IA所在的部分区域,同样在转型成IB的过程中,指针将被移动到IB所在的部分区域(也就是说,转型之后,指针的值都不一样)。在之后的操作中,pA操作的便是IA这个部分中的IBase,而pB操作的便是IB这个部分中的IBase,最后IA部分中的IBase变成了1,而IB部分中的IBase变成了-1。所以输出的结果也就变成了1和-1了。之后我们修改成了虚继承,看看到底发生了什么?cimpl-memory-virtual:原来的IA和IB中的IBase部分变成了一个指向基类的指针,而基类也变成了一个单独的部分。这样一旦对基类做任何的修改,都会通过这个指针重定向到这个独立的基类上去,于是,就不存在多副本的数据存储了,这个诡异的问题也就解决了。但是当然从这个图上我们也可以看到,使用虚继承后,访问数据多了一次跳转,这多出的一次跳转将导致效率的下降一倍甚至更多,所以如果一个类使用的非常频繁,很明显应该尽量避免使用虚继承。二义性当然数据的存储只是使用多重继承中遇到的一个问题,现在我们来看另外一个问题,函数的二义性。首先我们先把数据的存储抛开,单纯的来看一个只有函数的继承关系。?1234567891011121314151617181920212223242526272829303132class IBasepublic:virtual IBase() void foo() ;class IA : public IBasepublic:virtual IA() ;class IB : public IBasepublic:virtual IB() ;class CImpl : public IA, public IBpublic:virtual CImpl() ;int main(int argc, char* argv)CImpl o;o.foo(); / 直接调用CImpl的foo函数return 0;编译一下,试试。error C2385: ambiguous access of foocould be the foo in base IBaseor could be the foo in base IBaseerror C3861: foo: identifier not found出错了!杯具。为什么?错误还这么奇怪,神马叫做可以是IBase中的foo又可以是IBase中的foo呢?这就是使用多重继承的时候经常产生的第二个问题:二义性。在使用多重继承时,如果有两个被继承的类拥有共同的基类,那么就很容易出现这种情况。那什么是二义性呢?我们先来看一个更简单的继承关系:?1234567891011121314151617class Apublic:void foo();class B : public Apublic:void foo();class C : public Bpublic:void foo();我们可以把继承关系中,两个类之间沿着基类方向的相隔的继承级数看成一个距离,那么C到A的距离是2,B到A的距离就是1。当然距离不能为负。当我们对ABC中某个对象调用foo函数的时候,编译器会优先选择离当前指针类型的距离最短的一个函数实现去调用,也就是说,foo函数的查找路径是C-B-A,找到一个最近的去调用。而对于我们当前这个继承关系来说,IA和IB还是各包含一份IBase的实例,虽然在内存里这里仅仅是包含一份数据,但是在编译的过程中,IA和IB中还包含了一份从IBase中继承下来的函数列表。所以有两个包含有foo函数类与CImpl类的距离是一样的,所以在对CImpl调用foo函数,就产生了所谓的二义性,除非我们指定使用IA:foo或者IB:foo,否则编译器将无法决定使用哪一个基类的foo函数。?1o.IA:foo(); / 指定调用CImpl从IA部分继承过来的foo函数,这样就可以编译通过了。当然如果我们这样写代码也是不行的:?12IBase *pBase = &o; / 指针转义时的二义性,不知道是使用IA中的IBase部分,还是IB中的IBase部分o.inc(); / 数据访问时的二义性,不知道是访问IA中IBase部分的n,还是IB中IBase部分的n多重继承中的虚函数既然直接使用多重继承会有如此多的问题,那么我们能不能通过虚函数来解决这个问题呢?这里小小的提一下,刚刚二义性里面说到两个类的距离,对于编译器来说,一般是找离当前的类距离最近的函数实现来调用(或者数据来访问),而虚函数则是让编译器做相反的事情:找一个离当前类反向距离最远的函数实现来调用。好,我们先把上面的程序做一点点小改变,把foo()函数变成一个虚函数,看看有什么变化。?123456class IBasepublic:virtual IBase() virtual void foo() / 变成虚函数了;编译,结果还是失败。error C2385: ambiguous access of foocould be the foo in base IBaseor could be the foo in base IBaseerror C3861: foo: identifier not found产生问题的原因依然是二义性。即便换成virtual函数,也不能改变二义性这个问题。为什么呢?因为我们是用的.运算符来访问的,而不是用指针,所以这里虚函数和普通函数没有任何区别。=.=。好,我们再来小小的修改一下,把他变成指针,让他通过虚表去访问,看看行不行。?12CImpl *p = &o;p-foo();编译,结果。还是一样失败。好吧,我们可以把调用foo()的几句话都去掉,来看看CImpl中生成的虚表到底是个什么样子。debug-result-vptr-1:在这个实例中,IA和CImpl部分公用一个虚表,而IB则使用另外的一个虚表(两个虚表这个特性主要是在指针类型转换的时候有用,这里就不说了)。在这IA的虚表中存在一个指向IBase:foo()的指针,在IB的虚表中也存在一个指向IBase:foo()的指针,所以在CImpl中,可以找到两个IBase:foo()函数的指针,这样,编译器就无法确定到底应该使用哪一个IBase:foo()函数作为他自己的foo()函数了。二义性也就产生了。既然如此,那解决起来就没有什么别的办法了,只能把foo函数的最终在CImpl中实现一次了。?1234567891011121314151617class CImpl : public IA, public IBpublic:virtual CImpl() virtual void foo() ;int main(int argc, char* argv)CImpl o;o.foo();CImpl *p = &o;p-foo();return 0;编译一下,通过了!对于o.foo()来说,这当然是意料之中,离CImpl距离最近的foo函数实现,就是CImpl自己嘛,当然没有问题。对于后面这个p-foo()的调用,编译器现在也已经可以决定对于CImpl这个类来说,离他最远的foo函数调用是谁了也是他自己。所以这里就不会产生二义性的问题了。在多重继承中编译器对this指针的修正这里再让我们来看看这次编译出来的虚表,看看还有什么发现。debug-result-vptr-2:0x004112a3 thunk:CImpl:fooadjustor4 (void) *这个看上去很怪的函数是什么呢?我们反汇编一下他看看。virtual-function-wrapper:这里可以看到有一句汇编指令:sub ecx, 4。这条指令的左右其实是在修正this指针。因为从IB的虚表来的请求,this指针都是指向CImpl中IB的部分,而当调用CImpl中的foo函数时,如果还使用IB的this指针,那么程序就会出错,所以这里需要先将this指针修正为CImpl所在的地址,才能调用CImpl的foo函数。在程序运行的时候,this指针一般被存储在ecx寄存器中,或者当前函数的第一个参数传递进去,不过不同的语言或者不同的编译器编译出来的代码可能会不一样。我们这里的析构函数都是虚函数,所以我们还可以在截图中看到,编译器会对析构函数做同样的处理。如何同时解决数据访问和二义性问题呢貌似到现在都只提到最简单的一种多重继承的情况,但是实际上我们已经遇到了很多的问题了,既然多重继承中会有这么多问题,那我们有没有什么比较通用的方法能把他们一起解决了呢?方法肯定是有的:1. 使用虚继承这算是一种确实可行的方法,只是说会带来额外的时间和空间的开销,访问任何一个数据,都需要通过虚继承表进行跳转,不过一般来说够用了。2. 虚函数当接口,继承多个接口,统一实现这个思想就类似于COM了,只是说COM用的是纯虚函数,对于那些会产生二义性的类,我们在最后都实现一边,这样就不会有问题了。这样带来的时间开销也仅仅是调用时查询一次虚表。但是麻烦的地方就是,有时候继承一下,你可能就要实现一下了,比如引用计数神马的,当然你也可以通过模版来简化你的代码。?1234567891011121314151617181920

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论