《第4章多态性精选文档.ppt》由会员分享,可在线阅读,更多相关《第4章多态性精选文档.ppt(88页珍藏版)》请在taowenge.com淘文阁网|工程机械CAD图纸|机械工程制图|CAD装配图下载|SolidWorks_CaTia_CAD_UG_PROE_设计图分享下载上搜索。
1、第4章多态性本讲稿第一页,共八十八页主要内容1.静态联编和动态联编2.运算符重载3.虚函数4.多态性带来的好处本讲稿第二页,共八十八页4.1概述所谓多态性多态性,就是不同对象收到相同的消息时,产生不同的动作。通俗地说,多态性是指用一个名字定义不同的函数,这些函数执行不同但又类似的操作,即用同样的接口访问功能不同的函数,从而实现“一个接口,多种方法”。本讲稿第三页,共八十八页1.几个重要的概念联编联编(Building):将函数调用链接上相应的函数体的代码,这一过程称为联编。静态联编静态联编(StaticBinding):是指系统在编译时就决定如何实现某一动作。即:静态联编要求在程序编译时就知道
2、调用函数的全部信息。优点优点:函数调用速度很快。动态联编动态联编:是指系统在运行时动态实现某一动作。即:采用动态联编时,系统要到程序运行时才能确定调用哪个函数。优点优点:提供了更好的灵活性、问题抽象性和程序易维护性。本讲稿第四页,共八十八页2.静态联编静态联编支持的多态性称为编译时多态性编译时多态性。在C+中编译时多态性是通过函数重载函数重载和模板模板体现的。利用函数重载机制,在调用同名的函数时,编译系统可以根据实参的具体情况确定所调用的是同名函数中的哪一个。利用函数模板,编译系统可以根据模板实参以及模板函数实参的具体情况确定所要调用的是哪个函数,并生成相应的函数实例;利用类模板,编译系统可以
3、根据模板实参的具体情况确定所要定义的是哪个类的对象,并生成相应的类实例。例例:见书第298页例10.1。本讲稿第五页,共八十八页例10-1重载函数的静态联编o在主函数main()中,对这些重载的成员函数有6处调用,这些调用反映了编译系统在完成联编时,区分重载函数的3种方法:o(1)根据实参特征来区分。例如,程序中的objD.Set(5);和objD.Set(A);o一个实参为整型常量5,一个为字符型常量A。o(2)根据对象的类型来区分。例如,程序中的oobjB.Who();和objD.Who();oobjB是类Base的对象,而objD是类Derived的对象,编译程序在编译时能够根据对象的类
4、型,来决定调用哪个函数。o(3)使用作用域分辩符加以区分。例如,程序中的oobjD.Base:Show();和objD.Derived:Show();o前者是调用objD中的直接基类中的Show()函数,而后者是调用派生类Derived的Show()函数。通过使用作用域分辩符,编译程序在编译时就能区分调用哪个函数。当然,直接使用objD.Show();调用的也是派生类Derived中的成员函数Show(),这是编译系统按照同名覆盖的原则决定的。本讲稿第六页,共八十八页2.动态联编动态联编所支持的多态性称为运行时多态性运行时多态性。在C+中,运行时多态性是通过继承继承和虚函数虚函数来实现的。为了
5、达到动态联编获得运行时多态性的效果,通常都是用指向基类的指针来调用派生类的虚函数。例例:见书第300页例10.2。本讲稿第七页,共八十八页派生类对象的地址赋值给指向基类的指针o#includeousingnamespacestd;oclassBaseopublic:ovoidWho()coutIambaseclass!endl;oclassDerived1:publicBaseopublic:ovoidWho()coutIamDerived1class!endl;oclassDerived2:publicBaseopublic:ovoidWho()coutIamDerived2class!Wh
6、o();op=&one_obj;o/基类指针p指向派生类Derived1的对象one_objop-Who();o one_obj.Who();op=&two_obj;o/基类指针p指向派生类Derived2的对象two_objop-Who();otwo_obj.Who();oreturn 0;oo程序的运行结果为:oI am base class!oI am base class!WHY?oI am Derived1 class!oI am base class!WHY?oI am Derived2 class!本讲稿第八页,共八十八页程序分析程序分析:从程序运行结果可以看出,指向基类的指针p
7、,不管赋给它的是基类对象base_obj的地址还是派生类对象one_obj和two_obj的地址,语句“p-Who();”调用的始终是基类中定义的版本。很明显,在这种情况下通过基类指针输出的结果并不是我们所需要的结果。我们希望指针p指向了对象one_obj后,输出的结果应为“IamDerived1class!”。造成以上结果的不统一原因是:在派生类Derived1中虽然继承了基类Base的成员函数Who(),但在派生类中又定义了一个同名的Who()函数,而这种改变在静态联编的条件下编译器并不知道。问题问题:那么,怎样获得我们所希那么,怎样获得我们所希望的结果呢?望的结果呢?本讲稿第九页,共八十
8、八页解决方案解决方案:必须通知编译器这种可能的改变,即需要进行动动态联编态联编。为了实现动态联编,C+提出了虚函数虚函数的概念。o虚函数:虚函数在基类中定义,在基类成员函数声明的前面加上virtual关键字(在派生类中重新定义该函数时不用加关键字virtual),即可把该函数声明为虚函数。本讲稿第十页,共八十八页将基类中某个成员函数声明为虚函数,并在派生类中重新定义从基类继承下来的虚函数后,当把派生类对象的地址赋给指向基类的指针变量时,用指向基类的指针变量来调用虚函数,则执行的就是派生类自己重新定义的函数版本。例例:将书第300页例10.2基类Base中的Who()函数定义为虚函数,即改写成v
9、irtualvoidWho()即可得到我们所希望的结果。I am base class!I am Derived1 class!I am Derived1 class!I am Derived2 class!I am Derived2 class!随着指针P实际指向的对象不同,使用语句P-Who();能够调用不同类中Who的相同版本,我们就可以用相同的界面P-Who()访问函数Who的多个实现版本,从而也就能够在程序运行时告诉用户,当时指针P实际指向何类对象。函数调用P-Who()依赖于程序运行时P的值。本讲稿第十一页,共八十八页o把例10-2基类中的Who()函数定义为虚函数,即把程序中第六
10、行的ovoidWho()coutIambaseclass!endl;o改为:ovirtualvoidWho()coutIambaseclass!Who();”,由于赋给p的对象地址不同,使得能够实现调用不同Who()函数版本的方法。由于所调用函数Who()的版本是在程序运行时确定的,因此称为动态联编,这种多态性也称为运行时的多态性。本讲稿第十二页,共八十八页4.2运算符重载引述引述:通常,在进行面向对象的程序设计时使用两种形式的重载:函数重载函数重载:是指在相同作用域内,若干个参数特征不同的函数使用相同的函数名,因此也称为函数名重载。(已经讨论过)运算符重载运算符重载:是指同样的运算符可以施加
11、于不同类型的操作数上面。当然,当参数特征不同或被操作数的类型不同时,实现函数的算法或运算符的语义往往是不同的。(本章讨论)本讲稿第十三页,共八十八页1.运算符重载的方法和规则引例引例:见书第303页例10.3。#includeusingnamespacestd;classComplexpublic:Complex(doubler=0.0,doublei=0.0)real=r;imag=i;ComplexAdd(Complex&c);/复数的加法运算复数的加法运算ComplexSub(Complex&c);/复数的减法运算复数的减法运算voiddisplay();/复数输出复数输出private
12、:doublereal;doubleimag;本讲稿第十四页,共八十八页ComplexComplex:Add(Complex&c)returnComplex(real+c.real,imag+c.imag);ComplexComplex:Sub(Complex&c)returnComplex(real-c.real,imag-c.imag);voidComplex:display()cout(real,imag)endl;本讲稿第十五页,共八十八页intmain()Complexc1(5,10),c2(1.5,2.5),c3;coutc1=;c1.display();coutc2=;c2.di
13、splay();c3=c1.Add(c2);/c3=c1+c2;coutc1+c2=;c3.display();coutc1-c2;c3=c1.Sub(c2);/c3=c1-c2;c3.display();return0;本讲稿第十六页,共八十八页o使用函数调用方式完成复数的算术运算,与人们习惯相差较远。o能否在程序中使用人们习惯的表达式“c1+c2”来完成复数c1和c2的相加?对运算符“+”进行重载!o运算符重载是对已有的运算符赋予多重含义,使同一个运算符作用于不同类型的数据时,行为不同。本讲稿第十七页,共八十八页1)运算符重载的方法运算符重载的方法运算符重载的方法是定义一个重载运算符的函数
14、,在需要被执行重载的运算符时,系统就自动调用该函数,以实现相应的运算。即:运算符重载是通过定义函数实现的,其本质就是函数重载本质就是函数重载。o 实现过程:首先把指定的运算表达式转化为对运算符函数的调用,运算对象转化为运算符函数的实参,然后根据实参的类型来确定需要调用的函数,这个工作是在编译过程中完成的。重载后运算符对自定义类型操作数的处理,实际上是通过函数调用来完成的,是函数调用的一种特殊形式。我们把用来重载运算符的成员函数或友元函数,统称为运算符函数。本讲稿第十八页,共八十八页运算符的重载形式有以下两种:重载为类的成员函数;重载为类的友元函数。运算符重载为类的成员函数运算符重载为类的成员函
15、数o一般语法形式为:函数类型operator运算符(形参表列)函数体;/对运算符的重载处理2)运算符的重载形式 “函数类型函数类型”指定了重载运指定了重载运算符的返回值类型,也就是运算符的返回值类型,也就是运算结果类型。算结果类型。“operator”是定义运算符是定义运算符重载函数的关键字。重载函数的关键字。“运算符运算符”给定了要重给定了要重载的运算符名称,必须是载的运算符名称,必须是C+中可重载的运算符。中可重载的运算符。o必须放在类X的公有段本讲稿第十九页,共八十八页参数表中列出该运算符所需要的操作数,如下所述:(1)重载一元(一目)运算符时,参数表为空。在这种情况下,当前对象(即,调
16、用该运算符函数的对象)作为该运算符唯一的操作数。(2)重载二元(二目)运算符时,参数表中有一个参数。在这种情况下,当前对象作为该运算符的左操作数,参数作为右操作数。不难看出,当使用成员函数重载运算符时,定义运算符函数的方法与定义普通成员函数的方法基本相同,唯一差别是,运算符函数的名字必须为operator(为任何一个被重载的运算符),而不能由程序员随意为运算符函数起名字。本讲稿第二十页,共八十八页重载后运算符的语义:(1)一元运算符的语义。如果针对类X重载了一元运算符,xobj是X类的一个对象,则xobj或xobj的语义为:xobj.operator()(2)二元运算符的语义如果针对类X重载了
17、二元运算符,而xobj1和xobj2是X类的两个对象,则xobj1xobj2的语义为:xobj1.operator(xobj2)本讲稿第二十一页,共八十八页例例:复数类加减法运算重载为类的成员函数。classComplexpublic:Complex(doubler=0.0,doublei=0.0)real=r;imag=i;Complexoperator+(Complex&c2);/运算符运算符“+”重载为成员函数重载为成员函数Complexoperator-(Complex&c2);/运算符运算符“-”重载为成员函数重载为成员函数voiddisplay();/复数输出复数输出private
18、:doublereal;doubleimag;本讲稿第二十二页,共八十八页ComplexComplex:operator+(Complex&c2)Complexc;c.real=real+c2.real;c.imag=imag+c2.imag;returnc;ComplexComplex:operator-(Complex&c2)Complexc;c.real=real-c2.real;c.imag=imag-c2.imag;returnc;voidComplex:display()cout=0)cout+;coutimagiendl;本讲稿第二十三页,共八十八页intmain()Comple
19、xc1(5,10),c2(1.5,2.5),c3;coutc1=;c1.display();coutc2=;c2.display();c3=c1+c2;/c1+c2解释为解释为c1.operator+(c2)coutc1+c2=;c3.display();coutc1-c2=;(c1-c2).display();/c1-c2解释为解释为c1.operator-(c2)return0;本讲稿第二十四页,共八十八页o例10-5单目运算符“+”的重载oTimeoperator+();/声明前置自增运算符“+”重载函数(+i)一元运算符oTimeoperator+(int);/声明后置自增运算符“+”
20、重载函数(j+)二元运算符nC+语言的新标准规定,为了把运算符+(或-)重载为前缀形式,应该把它当作一元运算符来重载。把运算符+(或-)重载为后缀形式,应该把它当作二元运算符来重载。o程序中,重载后置自增运算符时,多了一个int型的参数,增加这个参数只是为了与前置自增运算符重载函数有所区别,此外没有任何作用。本讲稿第二十五页,共八十八页运算符重载为类的友元函数运算符重载为类的友元函数般语法形式为:friend函数类型operator运算符(形参表列)函数体;/对运算符的重载处理本讲稿第二十六页,共八十八页用友元函数重载运算符用友元函数重载运算符与成员函数重载相比主要有以下几点差别:(1)在函数
21、原型前多了一个关键字friend。(2)由于是友元函数,因此在类X的公有段和私有段说明此函数的效果相同(而若作为成员函数说明运算符函数,则必须放在类X的公有段)。(3)参数个数问题:由于这样说明的运算符函数是类X的友元函数而不是成员函数,因此它没有this指针。由此,必须在参数表中显式列出每一个操作数。因此,用友元函数重载运算符时参数表中参数个数,比用成员函数重载运算符时参数表中的参数个数多一个。n重载一元运算符时参数表中应该有一个参数;n重载二元运算符时参数表中应该有两个参数,其中第一个参数作为左操作数,第二个参数作为右操作数。本讲稿第二十七页,共八十八页用友元函数重载运算符用友元函数重载运
22、算符(1)一元运算符的语义。如果在类X中重载了一元运算符,xobj为X类的一个对象,则xobj或xobj的语义为:operator(xobj)(2)二元运算符的语义。如果在类X中重载了二元运算符,而xobj1和xobj2是X类的两个对象,则xobj1xobj2的语义为:operator(xobj1,xobj2)本讲稿第二十八页,共八十八页例例:将复数加减法运算重载为友元函数。o例10-6将复数加法运算重载为友元函数nfriendComplexoperator+(Complexc1,Complexc2);/运算符“+”重载为友元函数nfriendComplexoperator+(Complexc
23、1,doublec2);/运算符“+”重载为友元函数nfriendComplexoperator+(doublec1,Complexc2);/运算符“+”重载为友元函数n若要修改c1或c2对象的值,则必须用引用参数:nComplex&c1或Complex&c2本讲稿第二十九页,共八十八页例例:将复数加减法运算重载为友元函数。classComplexpublic:Complex(doubler=0.0,doublei=0.0)real=r;imag=i;/运算符运算符“+”重载为友元函数重载为友元函数friendComplexoperator+(Complex&c1,Complex&c2);/运
24、算符运算符“-”重载为友元函数重载为友元函数friendComplexoperator-(Complex&c1,Complex&c2);/复数输出复数输出voiddisplay();private:doublereal;doubleimag;本讲稿第三十页,共八十八页Complexoperator+(Complex&c1,Complex&c2)returnComplex(c1.real+c2.real,c1.imag+c2.imag);Complexoperator-(Complex&c1,Complex&c2)returnComplex(c1.real-c2.real,c1.imag-c2.
25、imag);voidComplex:display()cout=0)cout+;coutimagiendl;本讲稿第三十一页,共八十八页intmain()Complexc1(5,10),c2(1.5,2.5),c3;coutc1=;c1.display();coutc2=;c2.display();c3=c1+c2;/c1+c2 解释为解释为operator+(c1,c2)coutc1+c2=;c3.display();c3=c1-c2;/c1-c2 解释为解释为operator-(c1,c2)cout”。下列两个运算符只能用友元函数重载,不能用成员函数重载:“”。本讲稿第三十四页,共八十八页
26、3)运算符重载的规则_IC+不允许用户自己定义新的运算符,只能对已有的C+运算符进行重载。C+中允许重载的运算符如P306表10-1所示。不能重载的运算符有以下五个:成员访问运算符“.”;成员指针访问运算符“.*”;域运算符“:”;长度运算符sizeof;条件运算符“?:”。本讲稿第三十五页,共八十八页运算符或者是类的成员函数,或者有一个参数的类型是这个类或者是对这个类的引用。在进行运算符重载时,需要注意:重载的运算符至少有一个参数的类型与自定义的类有关,即重载的运算符的参数不能全部重载的运算符的参数不能全部是预定义的数据类型是预定义的数据类型。3)运算符重载的规则_II本讲稿第三十六页,共八
27、十八页例例:classComplexpublic:Complex(doubler=0.0,doublei=0.0)real=r;imag=i;Complexoperator+(Complex&c2);/运算符运算符“+”重载为成员函数重载为成员函数friendComplexoperator+(Complexc1,doubler);/运算符运算符“+”重载为友元函数重载为友元函数friendComplexoperator+(doubler,Complexc1);/运算符运算符“+”重载为友元函数重载为友元函数voiddisplay();/复数输出复数输出private:doublereal;do
28、ubleimag;说明说明:不能进行如下形式的重载:不能进行如下形式的重载:int operator+(int,int);/错误错误本讲稿第三十七页,共八十八页3)运算符重载的规则_III重载时,运算符的优先级、结合性以及操作数的个数都不能改变。说明说明:l由于重载不能改变运算符对象(操作数)的个数。因此,定义为类的成员函数的单目运算符单目运算符没有参数,而定义为类的友元函数时则有一个参数。同样,双目运算符双目运算符如果被定义为类的成员函数,则有一个参数;如果被定义为类的友元函数,则有两个参数。l重载运算符不能有默认参数。因为如果有默认参数,则会改变运算符的参数个数。本讲稿第三十八页,共八十八
29、页3)运算符重载的规则_IV除赋值运算符外,所有其他重载的运算符都可以被派生类继承。应当使重载运算符的功能类似于该运算符作用于标准类型数据时所实现的功能。本讲稿第三十九页,共八十八页重载赋值运算符指针悬挂问题includeclassStringchar*p;intsize;public:String(intsz)p=newcharsize=sz;String()deletep;本讲稿第四十页,共八十八页voidmain()Strings1(10);Strings2(20);s1=s2;/使用缺省的赋值运算符使用缺省的赋值运算符operator=上例中,由于s1.p和s2.p具有相同值,都指向s
30、2的字符串,s1.p原先指向的内存区不仅没有释放,而是被封锁起来无法再用,这就是所谓的指针悬挂问题指针悬挂问题。更严重的问题是,由于s1.p和s2.p都指向同一块内存区,当s1和s2这两个对象生存期结束时,将调用两次析构函数,从而使这块内存被释放两次,这是一个非常严重的错误。本讲稿第四十一页,共八十八页C+语言规定,赋值运算符必须使用成员函数重载,而且重载赋值运算符的成员函数operator=不能被继承。解决方法解决方法:在赋值之前,先释放s1.p原先指向的内存空间。因此必须重载赋值运算符,使得对目的对象数据成员指针p的赋值,是把源对象指针p所指向的内容传递给它,而不是简单地传递指针值。通过重
31、载赋值运算符,在运算符函数operator=的定义中,完成释放工作。如下例所示。本讲稿第四十二页,共八十八页#includeclassStringchar*p;intsize;public:String(intsz)p=newcharsize=sz;String()deletep;voidoperator=(String&);/运算符函数运算符函数;voidStringoperator=(String&a)if(this=&a)return;/防止防止a=a;deletep;/否则,释放目的对象原先占用的内存否则,释放目的对象原先占用的内存p=newcharsize=a.size;/重新给目的
32、对象分配内存重新给目的对象分配内存strcpy(p,a.p);/传递指针所指向的内容传递指针所指向的内容本讲稿第四十三页,共八十八页4.3虚函数1.虚函数的定义及使用1)定义:声明虚函数的方法是在基类中的成员函数原型前加上关键字virtual。其格式如下:class类名virtual类型成员函数名(参数表);C+通过虚函数实现通过虚函数实现运行时的多态性。运行时的多态性。本讲稿第四十四页,共八十八页说明说明:必须首先在基类中声明虚函数。派生类中与基类虚函数原型完全相同完全相同(包括函数名、返回类型、参数个数和参数类型的顺序)的成员函数,即使在说明时前面没有冠以关键字virtual也自动成为虚函
33、数。只有非静态成员函数非静态成员函数可以声明为虚函数。本讲稿第四十五页,共八十八页不允许在派生类中定义与基类虚函数名字及参数特征都相同,仅仅返回类型不同的成员函数。仅仅函数名相同但参数特征不同的函数,系统视为不同的函数(即函数的重载)。通过声明虚函数来使用C+提供的多态性机制时,派生类应该从它的基类公有派生公有派生。例例:见书第:见书第316页例页例10.7。本讲稿第四十六页,共八十八页2)虚函数的使用三要素在基类定义中,必须把成员函数定义为虚函数。在派生类定义中,对虚函数的更新定义只能修改函数体内容,而函数名、返回类型、参数个数、参数类型及参数顺序必须与基类的定义完全相同。必须用指向基类的指
34、针(或引用)访问虚函数。若仅仅是返回类型的不同,则若仅仅是返回类型的不同,则C+认为是错认为是错误的;如果是函数原型不同,仅函数名相误的;如果是函数原型不同,仅函数名相同,则同,则C+认为是一般的函数重载,此时认为是一般的函数重载,此时虚特性丢失。虚特性丢失。尽管可以像调用其他成员函数那样显式地尽管可以像调用其他成员函数那样显式地用对象名来调用一个虚函数,但只有在同一用对象名来调用一个虚函数,但只有在同一个指向基类的指针个指向基类的指针(或引用)访问虚函数时,访问虚函数时,运行时的多态性才能实现。运行时的多态性才能实现。本讲稿第四十七页,共八十八页例例:classApublic:virtual
35、voidshow()cout“AAA”endl;/必须有必须有virtual,否则不具备多态性,否则不具备多态性;classB:publicApublic:voidshow()cout“BBB”show();main()A*pa=newA;B*pb=newB;disp(pa);disp(pb);deletepa;deletepb;或者用或者用“void disp(A&a)”,但不,但不可用可用“void disp(A a)”,因为,因为使用普使用普通对象调用虚函数时,系统仍然以静态联通对象调用虚函数时,系统仍然以静态联编方式完成对虚函数的调用。编方式完成对虚函数的调用。请独立分析书第请独立分析
36、书第319页例页例10.8。本讲稿第四十九页,共八十八页3)虚函数的限制_I只有类的成员函数才能说明为虚函数。因为,虚函数仅适用于有继承关系的类对象,所以普通函数不能说明为虚函数。静态成员函数不能是虚函数。因为,静态成员函数不受限与某个对象,而虚函数调用要靠特定的对象来决定该激活哪个函数。内联函数不能是虚函数,因为内联函数是不能在运行中动态确定其位置的。虚函数即使在类的内部定义,编译时仍将其看作是非内联的。本讲稿第五十页,共八十八页3)虚函数的限制_II构造函数不能是虚函数,因为构造时对象还是一片未定型的空间。只有在构造完成后,对象才能成为一个类的名副其实的实例。析构函数可以是虚函数,而且通常
37、说明为虚函数。将析构函数定义为虚函数的目的在于:使用delete运算符删除一个对象时,能确保析构函数被正确的执行。这是因为,设置虚析构函数后,可以利用动态联编方式选择析构函数。本讲稿第五十一页,共八十八页虚析构函数o析构函数也可以是虚的,甚至是纯虚的。例如:classApublic:virtualA()=0;/纯虚析构函数;o本讲稿第五十二页,共八十八页当一个类打算被用作其它类的基类时,它的析构函数必须是虚的。o考虑下面的例子:classApublic:A()ptra_=newchar10;A()deleteptra_;/非虚析构函数private:char*ptra_;本讲稿第五十三页,共八
38、十八页oclassB:publicAoopublic:oB()ptrb_=newchar20;oB()deleteptrb_;oprivate:ochar*ptrb_;o;ovoidfoo()ooA*a=newB;odeletea;oo 在这个例子中,在执行delete a的时候,实际上只有A:A()被调用了,而B类的析构函数并没有被调用!程序不会象你想象的那样运行?o 如果将上面A:A()改为virtual,就可以保证B:B()也在delete a的时候被调用了。因此基类的析构函数都必须是virtual的。o 本讲稿第五十四页,共八十八页纯虚的析构函数纯虚的析构函数并没有什么作用,是虚的就够
39、了。通常只有在希望将一个类变成抽象类(不能实例化的类),而这个类又没有合适的函数可以被纯虚化的时候,可以使用纯虚的析构函数来达到目的。本讲稿第五十五页,共八十八页虚函数使用技巧1-private的虚函数o考虑下面的例子:o#includeoclassAoopublic:ovoidfoo()bar();oprivate:ovirtualvoidbar()couta;o;oclassB:publicAooprivate:ovirtualvoidbar()coutfoo();ooo在这个例子中,虽然bar()在A类中是private的,但是仍然可以出现在派生类中,并仍然可以与public或者prot
40、ected的虚函数一样产生多态的效果。并不会因为它是private的,就发生A:foo()不能访问B:bar()的情况,也不会发生B:bar()对A:bar()的override不起作用的情况。o 这种写法的语意是:A告诉B,你最好override我的bar()函数,但是你不要管它如何使用,也不要自己调用这个函数。oBB:bar动态联编本讲稿第五十六页,共八十八页2-构造函数和析构函数中的虚函数调用o一个类的虚函数在它自己的构造函数和析构函数中被调用的时候,它们就变成普通函数了,不“虚”了。也就是说不能在构造函数和析构函数中让自己“多态”。例如:本讲稿第五十七页,共八十八页oclassAoop
41、ublic:oA()foo();/在这里,无论如何都是A:foo()被调用!oA()foo();/同上ovirtualvoidfoo();o;oclassB:publicAoopublic:ovirtualvoidfoo();o;ovoidbar()ooA*a=newB;odeletea;oo 如果你希望delete a的时候,会导致B:foo()被调用,那么你就错了。同样,在new B的时候,A的构造函数被调用,但是在A的构造函数中,被调用的是A:foo()而不是B:foo()。本讲稿第五十八页,共八十八页3)虚函数的限制_III基类与派生类是相对的,因此,并非在任何情况下都必须首先在类等级
42、的最高层类内声明虚函数。在派生类中声明的成员函数vf2(int),与基类中的虚函数vf2()名字相同,但参数不同,是一般的函数重载,屏蔽了基类中的虚函数。当在一个派生类内没有重新定义虚函数时,则使用其基类的虚函数版本,且仍为虚函数。本讲稿第五十九页,共八十八页C+中虚函数的实现oC+中,虚函数(VirtualFunction)是通过一张虚函数表(VirtualTable)来实现的。简称为V-Table。在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、重载的问题,保证其真实反应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个
43、子类的时候,这张虚函数表就像一个地图一样,指明了实际所应该调用的函数。oo一个类存在虚函数,那么编译器就会为这个类生成一个虚表,在虚表里存放的是这个类所有虚函数的地址。当生成类对象的时候,编译器会自动的将类对象的前四个字节设置为虚表的地址,而这四个字节就可以看作是一个指向虚表的指针。虚表里依次存放的是虚函数的地址,每个虚函数的地址占4个字节。n1)虚函数按照其声明顺序放于表中。n2)父类的虚函数在子类的虚函数前面。本讲稿第六十页,共八十八页o假设我们有这样的一个类:oclassBaseopublic:ovirtualvoidf()coutBase:fendl;ovirtualvoidg()co
44、utBase:gendl;ovirtualvoidh()coutBase:hf();o由b所指的内存中的虚函数表的f()的位置已经被Derive:f()函数地址所取代,于是在实际调用发生时,是Derive:f()被调用了。这就实现了多态。本讲稿第六十三页,共八十八页多重继承(无虚函数重载)o假设有下面这样一个类的继承关系。注意:子类并没有重载复类的函数。子类实例中的虚函数表:o我们可以看到:o1)每个父类都有自己的虚表。o2)子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)o这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。本讲
45、稿第六十四页,共八十八页多重继承(有虚函数重载)o下图中,我们重载了父类的f()函数。下面是对于子类实例中的虚函数表的图:o我们可以看见,三个父类虚函数表中的f()的位置被替换成了子类的函数指针。这样,我们就可以任一静态类型的父类来指向子类,并调用子类的f()了。如:本讲稿第六十五页,共八十八页oDerived;oBase1*b1=&d;oBase2*b2=&d;oBase3*b3=&d;ob1-f();/Derive:f()ob2-f();/Derive:f()ob3-f();/Derive:f()ob1-g();/Base1:g()ob2-g();/Base2:g()ob3-g();/Ba
46、se3:g()本讲稿第六十六页,共八十八页2.纯虚函数和抽象类引例引例:在一个图形系统中,有一个一般的闭合图形类CClosedFigure,它有两个子类为多边形类CPolygon和圆类CCircle,多边形类又有两个子类为矩形类CRectangle和三角形类CTriangle。CClosedFigureCPolygonCCircleCRectangleCTriangle本讲稿第六十七页,共八十八页从概念上,闭合图形都可求周长和面积,也可以画出来。故这些类都应该有求周长的函数Perimeter()、求面积的函数Area()和画出来的函数Draw()。为了使用的方便,希望每个类中这三个函数的调用形
47、式是相同的。但在考虑如何实现该图形系统时发现:由于不知道CClosedFigure的数据如何表现,故它的Perimeter、Area和Draw函数无法实现。本讲稿第六十八页,共八十八页由4.1节可知:在C+中,由继承带来的多态是指一个基类的指针变量在程序运行的不同的时刻指向其不同子类的对象。这样,一个基类的指针变量就有一个静态类型静态类型(编译时决定),即基类的指针类型;有多个动态类型动态类型(运行时决定),即不同子类的指针类型。本讲稿第六十九页,共八十八页例例:CClosedFigure*p;/静态类型静态类型是CClosedFigure*,动态类型动态类型是CClosedFigure*p=
48、newCPolygon;/静态类型静态类型是CClosedFigure*,动态动态类型类型是CPolygon*p=newCRectangle;/静态类型静态类型是CClosedFigure*,动态动态类型类型是CRectangle*p=newCCircle;/静态类型静态类型是CClosedFigure*,动态类型动态类型是CCircle*p-Draw();究竟调用哪个究竟调用哪个Draw函数,由函数,由p的动态类型决定。的动态类型决定。本讲稿第七十页,共八十八页函数调用表达式p-Draw()的静态类型静态类型是:voidCClosedFigure:Draw()如果p指向CCircle的对象,
49、则p-Draw()的动态类型动态类型是:voidCCircle:Draw()应该调用voidCCircle:Draw()。故不能在编译时连接,只能运行时动态地连接到voidCCircle:Draw()上。因此,要使表达式因此,要使表达式p-Draw()通过编译检查,通过编译检查,类类CClosedFigure中就要有中就要有Draw函数,但又写函数,但又写不出不出Draw的实现。为解决这个问题,的实现。为解决这个问题,C+中引中引入了入了纯虚函数纯虚函数和和抽象类抽象类的概念。的概念。本讲稿第七十一页,共八十八页1.纯虚函数为了强制派生类重新定义其基类的虚函数,C+语言引入了纯虚函数纯虚函数(
50、PureVirtualFunction)的概念。o一般格式:virtual函数原型=0;例例:virtualvoidShowArea()=0;本讲稿第七十二页,共八十八页2.抽象类如果一个类中至少说明了一个纯虚函数,则把该类称为抽象类抽象类。例例:ClassCClosedFigure public:virtualvoidDraw()=0;virtualdoublePerimeter()=0;virtualdoubleArea()=0;;本讲稿第七十三页,共八十八页说明说明:抽象类没有完整的实现,故不能实例化不能实例化。抽象类只能作为其他类的基类,不能声明抽象类的实例,不能创建它的对象;但可以定