




已阅读5页,还剩10页未读, 继续免费阅读
版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
多态和虚表多态和虚表2011-06-09 09:45多态的这个概念稍微有点模糊,如果想在一开始就想用清晰用语言描述它,让读者能够明白,似乎不太现实,所以我们先看如下代码:/例程1#include iostream using namespace std;class Vehiclepublic:Vehicle(float speed,int total)Vehicle:speed=speed;Vehicle:total=total;void ShowMember()cout speed|total endl;protected:float speed;int total;class Car:public Vehiclepublic:Car(int aird,float speed,int total):Vehicle(speed,total)Car:aird=aird;void ShowMember()cout speed|total|aird endl;protected:int aird;void main()Vehicle a(120,4);a.ShowMember();Car b(180,110,4);b.ShowMember();cin.get();在c+中是允许派生类重载基类成员函数的,对于类的重载来说,明确的,不同类的对象,调用其类的成员函数的时候,系统是知道如何找到其类的同名成员,上面代码中的a.ShowMember();,即调用的是Vehicle:ShowMember(),b.ShowMember();,即调用的是Car:ShowMemeber();。但是在实际工作中,很可能会碰到对象所属类不清的情况,下面我们来看一下派生类成员作为函数参数传递的例子,代码如下:/例程2#include iostream using namespace std;class Vehiclepublic:Vehicle(float speed,int total)Vehicle:speed=speed;Vehicle:total=total;void ShowMember()cout speed|total endl;protected:float speed;int total;class Car:public Vehiclepublic:Car(int aird,float speed,int total):Vehicle(speed,total)Car:aird=aird;void ShowMember()cout speed|total|aird endl;protected:int aird;void test(Vehicle&temp)temp.ShowMember();void main()Vehicle a(120,4);Car b(180,110,4);test(a);test(b);cin.get();例子中,对象a与b分辨是基类和派生类的对象,而函数test的形参却只是Vehicle类的引用,按照类继承的特点,系统把Car类对象看做是一个Vehicle类对象,因为Car类的覆盖范围包含Vehicle类,所以test函数的定义并没有错误,我们想利用test函数达到的目的是,传递不同类对象的引用,分别调用不同类的,重载了的,ShowMember成员函数,但是程序的运行结果却出乎人们的意料,系统分不清楚传递过来的基类对象还是派生类对象,无论是基类对象还是派生类对象调用的都是基类的ShowMember成员函数。为了要解决上述不能正确分辨对象类型的问题,c+提供了一种叫做多态性(polymorphism)的技术来解决问题,对于例程序1,这种能够在编译时就能够确定哪个重载的成员函数被调用的情况被称做先期联编(early binding),而在系统能够在运行时,能够根据其类型确定调用哪个重载的成员函数的能力,称为多态性,或叫滞后联编(late binding),下面我们要看的例程3,就是滞后联编,滞后联编正是解决多态问题的方法。代码如下:/例程3#include iostream using namespace std;class Vehiclepublic:Vehicle(float speed,int total)Vehicle:speed=speed;Vehicle:total=total;virtual void ShowMember()/虚函数cout speed|total endl;protected:float speed;int total;class Car:public Vehiclepublic:Car(int aird,float speed,int total):Vehicle(speed,total)Car:aird=aird;virtual void ShowMember()/虚函数,在派生类中,由于继承的关系,这里的virtual也可以不加cout speed|total|aird endl;public:int aird;void test(Vehicle&temp)temp.ShowMember();int main()Vehicle a(120,4);Car b(180,110,4);test(a);test(b);cin.get();多态特性的工作依赖虚函数的定义,在需要解决多态问题的重载成员函数前,加上virtual关键字,那么该成员函数就变成了虚函数,从上例代码运行的结果看,系统成功的分辨出了对象的真实类型,成功的调用了各自的重载成员函数。多态特性让程序员省去了细节的考虑,提高了开发效率,使代码大大的简化,当然虚函数的定义也是有缺陷的,因为多态特性增加了一些数据存储和执行指令的开销,所以能不用多态最好不用。虚函数的定义要遵循以下重要规则:1.如果虚函数在基类与派生类中出现,仅仅是名字相同,而形式参数不同,或者是返回类型不同,那么即使加上了virtual关键字,也是不会进行滞后联编的。2.只有类的成员函数才能说明为虚函数,因为虚函数仅适合用与有继承关系的类对象,所以普通函数不能说明为虚函数。3.静态成员函数不能是虚函数,因为静态成员函数的特点是不受限制于某个对象。4.内联(inline)函数不能是虚函数,因为内联函数不能在运行中动态确定位置。即使虚函数在类的内部定义,但是在编译的时候系统仍然将它看做是非内联的。5.构造函数不能是虚函数,因为构造的时候,对象还是一片位定型的空间,只有构造完成后,对象才是具体类的实例。6.析构函数可以是虚函数,而且通常声名为虚函数。说明一下,虽然我们说使用虚函数会降低效率,但是在处理器速度越来越快的今天,将一个类中的所有成员函数都定义成为virtual总是有好处的,它除了会增加一些额外的开销是没有其它坏处的,对于保证类的封装特性是有好处的。对于上面虚函数使用的重要规则6,我们有必要用实例说明一下,为什么具备多态特性的类的析构函数,有必要声明为virtual。代码如下:#include iostream using namespace std;class Vehiclepublic:Vehicle(float speed,int total)Vehicle:speed=speed;Vehicle:total=total;virtual void ShowMember()cout speed|total endl;virtualVehicle()cout载入Vehicle基类析构函数endl;cin.get();protected:float speed;int total;class Car:public Vehiclepublic:Car(int aird,float speed,int total):Vehicle(speed,total)Car:aird=aird;virtual void ShowMember()cout speed|total|aird endl;virtualCar()cout载入Car派生类析构函数endl;cin.get();protected:int aird;void test(Vehicle&temp)temp.ShowMember();void DelPN(Vehicle*temp)delete temp;void main()Car*a=new Car(100,1,1);a-ShowMember();DelPN(a);cin.get();从上例代码的运行结果来看,当调用DelPN(a);后,在析构的时候,系统成功的确定了先调用Car类的析构函数,而如果将析构函数的virtual修饰去掉,再观察结果,会发现析构的时候,始终只调用了基类的析构函数,由此我们发现,多态的特性的virtual修饰,不单单对基类和派生类的普通成员函数有必要,而且对于基类和派生类的析构函数同样重要。详解虚表C+中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有多种形态,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。关于虚函数的使用方法,我在这里不做过多的阐述。大家可以看看相关的C+的书籍。在这篇文章中,我只想从虚函数的实现机制上面为大家一个清晰的剖析。当然,相同的文章在网上也出现过一些了,但我总感觉这些文章不是很容易阅读,大段大段的代码,没有图片,没有详细的说明,没有比较,没有举一反三。不利于学习和阅读,所以这是我想写下这篇文章的原因。也希望大家多给我提意见。言归正传,让我们一起进入虚函数的世界。虚函数表对C+了解的人都应该知道虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。在这个表中,主要是一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。这里我们着重看一下这张虚函数表。在C+的标准规格说明书中说到,编译器必需要保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证正确取到虚函数的偏移量)。这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。听我扯了那么多,我可以感觉出来你现在可能比以前更加晕头转向了。没关系,下面就是实际的例子,相信聪明的你一看就明白了。假设我们有这样的一个类:class Basepublic:virtual void f()coutBase:fendl;virtual void g()coutBase:gendl;virtual void h()coutBase:hendl;按照上面的说法,我们可以通过Base的实例来得到虚函数表。下面是实际例程:typedef void(*Fun)(void);Base b;Fun pFun=NULL;cout虚函数表地址:(int*)(&b)endl;cout虚函数表-第一个函数地址:(int*)*(int*)(&b)endl;/Invoke the first virtual function pFun=(Fun)*(int*)*(int*)(&b);pFun();实际运行经果如下:(Windows XP+VS2003,Linux 2.6.22+GCC 4.1.3)虚函数表地址:0012FED4虚函数表-第一个函数地址:0044F148 Base:f通过这个示例,我们可以看到,我们可以通过强行把&b转成int*,取得虚函数表的地址,然后,再次取址就可以得到第一个虚函数的地址了,也就是Base:f(),这在上面的程序中得到了验证(把int*强制转成了函数指针)。通过这个示例,我们就可以知道如果要调用Base:g()和Base:h(),其代码如下:(Fun)*(int*)*(int*)(&b)+0);/Base:f()(Fun)*(int*)*(int*)(&b)+1);/Base:g()(Fun)*(int*)*(int*)(&b)+2);/Base:h()注意:在上面这个图中,我在虚函数表的最后多加了一个结点,这是虚函数表的结束结点,就像字符串的结束符message一样,其标志了虚函数表的结束。这个结束标志的值在不同的编译器下是不同的。在WinXP+VS2003下,这个值是NULL。而在Ubuntu 7.10+Linux 2.6.22+GCC 4.1.3下,这个值是如果1,表示还有下一个虚函数表,如果值是0,表示是最后一个虚函数表。下面,我将分别说明无覆盖和有覆盖时的虚函数表的样子。没有覆盖父类的虚函数是毫无意义的。我之所以要讲述没有覆盖的情况,主要目的是为了给一个对比。在比较之下,我们可以更加清楚地知道其内部的具体实现。一般继承(无虚函数覆盖)下面,再让我们来看看继承时的虚函数表是什么样的。假设有如下所示的一个继承关系:请注意,在这个继承关系中,子类没有重载任何父类的函数。那么,在派生类的实例中,其虚函数表如下所示:对于实例:Derive d;的虚函数表如下:我们可以看到下面几点:1)虚函数按照其声明顺序放于表中。2)父类的虚函数在子类的虚函数前面。我相信聪明的你一定可以参考前面的那个程序,来编写一段程序来验证。一般继承(有虚函数覆盖)覆盖父类的虚函数是很显然的事情,不然,虚函数就变得毫无意义。下面,我们来看一下,如果子类中有虚函数重载了父类的虚函数,会是一个什么样子?假设,我们有下面这样的一个继承关系。为了让大家看到被继承过后的效果,在这个类的设计中,我只覆盖了父类的一个函数:f()。那么,对于派生类的实例,其虚函数表会是下面的一个样子:我们从表中可以看到下面几点,1)覆盖的f()函数被放到了虚表中原来父类虚函数的位置。2)没有被覆盖的函数依旧。这样,我们就可以看到对于下面这样的程序,Base*b=new Derive();b-f();由b所指的内存中的虚函数表的f()的位置已经被Derive:f()函数地址所取代,于是在实际调用发生时,是Derive:f()被调用了。这就实现了多态。(原来是这么一回事!)注意上面那句,是相应的函数被取代,而不是被覆盖的函数就会被放到第一位的位置。深入浅出MFC:1.每一个内涵虚函数的类,编译器都为它做出一个虚拟函数表,表中的每一个元素都指向一个虚函数的地址。此外,编译器当然也会为类表加上一项成员函数,是一个指向该虚拟函数表的指针(常被称为vptr),每一个由此类别派生出来的类,都有这么一个vptr。2.虚表以及这种间接呼叫方式。虚表的内容是依据类别中的虚函数声明次序-填入函数指针。派生类别会继承基础类别的虚表(以及所有其他可以继承的成员),当我们在派生类中改写虚函数时,虚表就受了影响;表中的元素所指的函数地址将不再是基类的函数地址,而是派生类的函数地址。当然,这里面所说的都是派生类继承了基类的虚表(具体点说也不能说是继承了虚表,而是基类虚表中有的,在派生类中叶有,并对派生类自己的虚函数进行了扩展),但是这是在VC环境或VS环境下的结果,对于不通的编译器,处理方式也不一样,如果是G+可能就不会继承。如:多重继承(无虚函数覆盖)下面,再让我们来看看多重继承中的情况,假设有下面这样一个类的继承关系。注意:子类并没有覆盖父类的函数。对于子类实例中的虚函数表,是下面这个样子:我们可以看到:1)每个父类都有自己的虚表。2)子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。多重继承(有虚函数覆盖)下面我们再来看看,如果发生虚函数覆盖的情况。下图中,我们在子类中覆盖了父类的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()安全性每次写C+的文章,总免不了要批判一下C+。这篇文章也不例外。通过上面的讲述,相信我
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 2025年中国微控制器I行业市场发展现状及投资方向研究报告
- 中国点读机市场竞争策略及行业投资潜力预测报告
- 共享农业机械服务商业计划书
- 中国工业用清洗剂行业调查报告
- 中国罐式汽车行业市场调研分析及投资战略咨询报告
- 2024年全球及中国金属爆炸复合材料行业头部企业市场占有率及排名调研报告
- 2024年中国天然气开采行业市场调查报告
- 中国3D打印光聚合材料行业竞争格局分析及投资战略咨询报告
- 101地板胶项目投资可行性研究分析报告(2024-2030版)
- 2025年中国盾构机行业发展趋势预测及投资战略咨询报告
- 贵州省政务信息化项目需求报告(建设类模板)、信息化建设项目实施方案模板2026年版(新建、升级改造)
- 2025年昆明市事业单位招聘考试综合类专业能力测试试卷(文秘类)真题解析
- 学堂在线 毛泽东思想和中国特色社会主义理论体系概论 章节测试答案
- 车间安全用电培训课件
- 2025至2030中国特医食品行业发展趋势分析与未来投资战略咨询研究报告
- 2024建安杯信息通信建设行业安全竞赛题库
- 水利水电工程行业市场发展分析及发展前景与投资研究报告2025-2028版
- 血小板减少症护理查房
- 浙江杭州市2024-2025学年高一下学期6月期末考试数学试题及答案
- 煤磨安全试题及答案
- 渐冻人麻醉处理要点
评论
0/150
提交评论