虚函数与多态性课件_第1页
虚函数与多态性课件_第2页
虚函数与多态性课件_第3页
虚函数与多态性课件_第4页
虚函数与多态性课件_第5页
已阅读5页,还剩233页未读, 继续免费阅读

下载本文档

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

文档简介

说明:1.多态性是OOP的重要组成部分。它是C++中的难点之一。C++的多态性是靠虚函数实现的。实际上虚函数使类的成员函数表现出多种形态。2.虚函数的形式十分简单,要领会其实质则不简单。第5章虚函数与多态性1说明:第5章虚函数与多态性1本章主要内容多态性的概念和多态类型联编虚函数抽象类运算符重载2本章主要内容多态性的概念和多态类型2多态性的概念多态性是指具有相似功能的不同函数使用同一个名称来实现,从而可以使用相同的调用方式来调用这些具有不同功能的同名函数的特性。C++的多态性是指对同一条消息,被不同类型的对象接收将产生不同的行为。多态性是实现“一种接口,多种方法”的技术。这里的“一种接口”是指相同函数名,而“多种方法”是指多种函数实现。3多态性的概念多态性是指具有相似功能的不同函数使用同一个名称联编多态性的实现过程中,确定调用哪个同名函数的过程就是联编(binding),又称绑定。联编是指计算机程序自身彼此关联的过程,也就是把一个标识符名和一个存储地址联系在一起的过程。用面向对象的术语讲,就是把一条消息和一个对象的方法相结合的过程。按照联编进行的阶段的不同,可以分为静态联编和动态联编,这两种联编过程分别对应着多态性的两种实现方式。4联编多态性的实现过程中,确定调用哪个同名函数的过程就是联编静态联编在编译阶段完成的联编称为静态联编。在编译过程中,编译系统可以根据类型匹配等特征来确定程序中调用操作与执行某个同名函数实现之间的关系,即确定某一个同名函数到底是要调用哪一段函数实现代码。函数重载和运算符重载就是通过静态联编方式实现的编译时的多态的体现。静态联编的优点是函数调用速度快、效率较高,缺点是编程不够灵活。5静态联编在编译阶段完成的联编称为静态联编。5动态联编只有在运行程序时才能确定将要调用的函数。这种在运行阶段进行的联编称为动态联编。动态联编的优点是提供了更好的编程灵活性、问题抽象性和程序易维护性,缺点是与静态联编相比,函数调用速度慢。6动态联编只有在运行程序时才能确定将要调用的函数。这种在运行虚函数虚函数是动态联编的基础。虚函数是非静态的成员函数,经过派生之后,虚函数在类族中可以实现运行时的多态性。7虚函数虚函数是动态联编的基础。7虚函数的声明虚函数是一个在某基类中声明为virtual并在一个或多个派生类中被重新定义的成员函数。声明虚函数的格式如下:virtual<返回值类型><函数名>(<参数表>);8虚函数的声明虚函数是一个在某基类中声明为virtual并在虚函数的使用如果某类中的一个成员函数被说明为虚函数,这就意味着该成员函数在派生类中可能有不同的函数实现。当使用对象指针或对象引用调用虚函数时,采用动态联编方式,即在运行时进行关联或绑定。9虚函数的使用如果某类中的一个成员函数被说明为虚函数,这就意例1classemployee{//...public:

virtualwhoami(){//…}//..};classmanager:publicemployee{//...public:

virtualwhoami(){//…}//..};10例1classemployee{//...10接上例classmanager:publicmanager{//...public:

virtualwhoami(){//…}//..};11接上例classmanager:publicmanag接上例voidmain(){employee*emp[3];emp[0]=newemployee;emp[1]=newmanager;emp[2]=newdirector;for(I=0;I<3;I++)emp[i]->whoami();//分别打印三种不同对象的whoami()函数。而不只是基类对象。}12接上例voidmain()12上述例子实现了一个whoami()函数有三种行为,即多态。这是关于多态性的语法方面的分析理解。为了进一步形象地理解多态,见下例:13上述例子实现了一个whoami()函数有三种行为,即多态例2classA{public:virtualvoidshow(){cout<<“classAshow()iscalled”<<endl;}};classB:publicA{public:voidshow(){cout<<“classBshow()iscalled”<<endl;}};14例2classA{public:virtualvoi接上例voidmain(){Ademo1,*p;Bdemo2;p=&demo1;p->show();//显示“classAshow()iscalled”p=&demo2;p->show();//显示”classBshow()iscalled”}//也表现了一个函数show()多种形态。同一样的语句,有不同的行为15接上例voidmain(){同一样的语句,有不同的行为15虚函数的语法总结说明1.虚函数的返回类型、名字、参数必须完全系统。2.只有类的成员函数才能是虚函数。这样才能与具体对象相联结,以实现不同类对象的同一函数的“多态”。3.静态成员函数不能说明为虚函数。16虚函数的语法总结说明1.虚函数的返回类型、名字、参数必须完全接上页4.虚函数不能是内联的。因为它实现了动态联编。假如在类内进行了定义,也认为它是非内联的。5.构造函数不能是虚函数。6.析构函数可以为虚函数,此时虚析构函数的名字为“~类型名”。以实现其“多态”地撤消不同类对象。17接上页4.虚函数不能是内联的。因为它实现了动态联编。假如在类接上页7.虚函数的“虚拟性”可以“继承”。即基类写了关键字virtual后,派生类的同名、同参数、同类型函数也是虚函数,可以不写virtual。多层继承时,如中间类不写虚函数,其后的派生类需要时重新定义即可。即不会出现虚拟继承的传递路径中断。8.注意虚函数与虚基类的区别。18接上页7.虚函数的“虚拟性”可以“继承”。即基类写了关键字接上页9.虚函数实现多态,只能通过指针或引用进行。因为只有这样,才能实现动态联编。10.多态可简单地理解为:同一名字多种含义。如:PLAYFOOTBALLBASKETBALLPINGPONGBALL19接上页9.虚函数实现多态,只能通过指针或引用进行。因为只有这再接上页11.派生类内可以调用基类的虚函数。但为了保持不破坏多态,一般不提倡这样做。12.虚函数的存取决定于首次定义它的基类中该函数是公有还是私有。如果是公有,派生类中仍然是公有;如果是私有,派生类中仍然是私有。13.注意虚函数与重载函数的区别。见下页20再接上页11.派生类内可以调用基类的虚函数。但为了保持不破坏虚函数与重载函数区别重载函数虚函数返回值参数不同任意静态不同同语义相关性适用范围联接名字同同动态派生类应语义相关可以语义无关同21虚函数与重载函数区别重载函数虚函数返回值参数不同任意静态不同多态形成的过程1.虚函数靠运行时动态联接来表现“多态”。即是根据不同的对象去调用不同类的虚函数,即实现了对象的多态;2.重载函数是靠参数匹配的差别,调用不同的函数。即重载函数无法实现“对象”的多态。尽管重载函数“看起来”似乎已经实现了多态。22多态形成的过程1.虚函数靠运行时动态联接来表现“多态”。即是C++提供的通过基类指针“自动”访问派生类的方法1.通过加类型标志成员来实现(C语言也能实现,这是一种常规技巧方法)2.通过虚函数(这是我们学习本章的主要目的)为了正确理解虚函数,以下先举用加标志成员的例子23C++提供的通过基类指针“自动”访问派生类的方法1.通过加类一.加标志成员方法加入的标志成员用来说明该基类指针所指对象究竟为哪一种类型对象。假设该标志成员为枚举类型(也可为其它类型)。程序见下例24一.加标志成员方法加入的标志成员用来说明该基类指针所指对象究例子enumempl_type{E,M,D};classemployee{public:empl_typetype;whoami(){//…}//...};classmanager:publicemployee{public:whoami(){//…}//...};classdirector:publicmanager{public:whoami(){//…}//...};25例子enumempl_type{E,M,D};25接上例main(){employee*emp[3];emp[0]=newemployee;//假设构造函数设置type为‘E’emp[1]=newmanager;//假设构造函数设置type为‘M’emp[2]=newdirector;//构造函数设置type为‘D’for(i=0;i<3;i++)switch(emp[i]->type){caseE:emp[i]->whoami();break;caseM:((manager*)emp[i])->whoami();break;caseD:((director*)emp[i])->whoami();break;}}基类指针强制转换为派生类指针26接上例main(){employee*emp[3];基类本例中的new说明newemployee;newmanager;newdirector;均自动调用该类的构造函数27本例中的new说明newemployee;均自纯虚函数在C++中,有一个仅为该多态机制提供一个接口而没有任何实体定义的函数,被称为纯虚函数。有时,基类的虚函数并不做任何事情。纯粹用于派生类的虚函数的定义需要,称为纯虚函数。C++规定,纯虚函数的专用书写格式为:virtual<返回值类型><函数名>(<参数表>)=0;28纯虚函数在C++中,有一个仅为该多态机制提供一个接口而没有抽象基类含有一个以上纯虚函数的类,被称为抽象基类,简称抽象类。如:classA{public:virrtualvoidprint()=0;//…};29抽象基类29特点:1.抽象基类不能生成对象。但可以定义指向它的指针。如:A*pa;2.当基类中有纯虚函数时,派生类中一定要定义该虚函数,否则将是错误的。30特点:1.抽象基类不能生成对象。但可以定义指向它的指针。如:抽象类抽象类是一种特殊的类,专门作为基类派生新类,自身无法实例化,也就是无法定义抽象类的对象,它为一类族提供统一的操作界面。抽象类是为了抽象和设计的目的而建立的,可以说,建立抽象类,就是为了通过它多态地使用其中的成员函数。抽象类处于类层次的上层,由它派生新类,然后再实例化。31抽象类抽象类是一种特殊的类,专门作为基类派生新类,自身无法抽象类抽象类只能用作其他类的基类,不能建立抽象类对象。抽象类不能用作参数类型、函数返回值类型或显式转换的类型,但可以说明指向抽象类的指针或引用,该指针或引用可以指向抽象类的派生类,进而实现多态性。抽象类的主要作用是将有关的派生类组织在一个继承层次结构中,由抽象类为它们提供一个公共的根,相关的派生类就从这个根派生出来。32抽象类抽象类只能用作其他类的基类,不能建立抽象类对象。3第七节虚函数应用举例由于任何图形(象)的制作都可以由点、线段、圆、椭圆、矩形等一些基本的图形单位组合而成。考虑这一事实,用虚函数的多态性原理实现作图的基本操作。33第七节虚函数应用举例由于任何图形(象)的制作都可以由点总体思路将每种基本图形看作为一个对象,这些对象形成继承关系。不同基本图形的具体“含义”由其虚函数定义,从而实现“多态”地画基本图形。步骤:1.需要考虑的细节是建立图形的最小对象---点。2.然后,建立画图形“基类”。其它图形单位都从其继承。3.利用虚函数实现各种基本图形单位的多态。34总体思路将每种基本图形看作为一个对象,这些对象形成继承关系。第一步建立点类classpoint{intx,y;public:point(){x=y=0;}point(intx1,y1){x=x1;y=y1;}point(constpoint&p){x=p.x;y=p.y}intgetx(){returnx;}intgety(){returny;}};35第一步建立点类classpoint{intx,y;3第二步建立作图的基类classshape{intcolor;public:shape(intcolor1){color=color1;}virtualvoiddraw(){cout<<“基类---不画具体的图形”<<endl;}};36第二步建立作图的基类classshape{intco第三步建立线段类classline:publicshape{pointstart,end;public:line(points,pointe,intcolor1):shape(color1),start(s),end(e){cout<<“根据start,end已生成作线段的对象”<<endl;}virtualvoiddraw(){cout<<“根据start,end的值作线段”<<endl;}};37第三步建立线段类classline:publicsh第四步建立矩形类classrectangle:publicshape{pointupperleft,lowerright;public:rectangle(pointul,pointlr,intcolor1):shape(color1),upperleft(ul),lowerright(lr){cout<<“根据upperleft,lowerright已生成作矩形的对象”<<endl;}virtualvoiddraw(){cout<<“根据upperleft,lowerright的值作矩形”<<endl;}};38第四步建立矩形类classrectangle:publ第五步建立圆类classcircle:publicshape{pointcenter;intradius;public:circle(pointctr,intr,intcolor1):shape(color1),center(ctr){cout<<“根据center,radius已生成作圆的对象”<<endl;}virtualvoiddraw(){cout<<“根据center,radius的值作圆”<<endl;}};39第五步建立圆类classcircle:publics第六步建立正方形类(从矩形类派生)classsquare:publicrectangle{public:sauare(pointul,intp,intcolor1):rectangle(ul,point(ul.getx()+p,ul.gety()+p,color1){cout<<“根据给定的左上角ul,正方形的长度p已生成作正方形的对象”<<endl;}virtualvoiddraw(){cout<<“根据给定的左上角ul,正方形的长度p的值作正方形”<<endl;}};注意:派生来自于矩形40第六步建立正方形类(从矩形类派生)classsquar第七步利用定义的类生成对象可以任意多态作图,见主调函数voidmain(){shape*pdraw;pdraw=newline(point(1,1),point(20,19),1);pdraw->draw();pdraw=newcircle(point(100,75),60,2);pdraw->draw();画线段画圆41第七步利用定义的类生成对象可以任意多态作图,见主调函接上页pdraw=newrectangle(point(1,1),point(200,190),3);pdraw->draw();pdraw=newsauare(point(1,1),60,4);pdraw->draw();}画矩形画正方形42接上页pdraw=newrectangle(point(运算符重载---引入

intx,y,z;z=x+y;//含义:整形的“+”运算数据类型均为整型共有两个运算符作用于整型运算43运算符重载---引入intx,y,z;数据类型均为整接上doublex,y,z;z=x+y;//含义:按实型的“+”运算数据类型均为实型共有两个运算符作用于实型运算44接上doublex,y,z;数据类型均为实型共有两个上图例说明通过分析得出结论:运算符实际上是对某种“类型”施加的操作。本例中的类型是C++预(先)定义的类型,即整型和实型。对以上结论推广:C++中,任何类型都应有适合该类型的运算符操作含义。见下例:45上图例说明通过分析得出结论:运算符实际上是对某种“类型”施加如下例。书写形式很直观、自然。看起来就象基本数据类型的运算一样。

classstudent{//…};studentx,y,z;z=x+y;数据类型均为student类型共有两个运算符作用于student类型运算含义:按student类型的约定进行两个对象的“加法(+)”运算46如下例。书写形式很直观、自然。看起来就象基本数据类型的运算一运算符重载运算符重载就是赋予系统预定义的运算符多重含义,使同一个运算符作用于不同类型的数据导致不同的行为。运算符重载的实质就是函数重载。运算符重载使得系统预定义的运算符能作用于用户自定义类型的数据,进行类似运算。47运算符重载运算符重载就是赋予系统预定义的运算符多重含义,使运算符重载规则C++的运算符除了少数几个之外,全部可以重载,而且只能重载已有的运算符,不可臆造新的运算符。重载之后运算符的优先级和结合性都不会改变,并且要保持原运算符的语法结构。参数和返回值类型可以重新说明。运算符重载是针对新类型数据的实际需要,对原有运算符进行适当的改造。运算符重载有两种方式:重载为类的成员函数和重载为类的友元函数。48运算符重载规则C++的运算符除了少数几个之外,全部可以重载当运算符重载为类的成员函数时,由于单目运算除了对象以外没有其它参数,因此重载“++”和“--”运算符,不能区分是前缀操作还是后缀操作。C++约定,在参数表中放上一个整型参数,表示后缀运算符。49当运算符重载为类的成员函数时,由于单目运算除了对象以外没有其运算符重载为成员函数运算符重载为类的成员函数的一般格式如下:其中,operator是定义运算符重载函数的关键字。<参数表>中最多有一个形参。<返回值类型>operator<运算符>(<参数表>){<函数体>;}50运算符重载为成员函数运算符重载为类的成员函数的一般格式如下运算符重载为友元函数运算符重载为类的友元函数的一般格式如下:其中,<参数表>最多有两个形参。friend<返回值类型>operator<运算符>(<参数表>){<函数体>;}51运算符重载为友元函数运算符重载为类的友元函数的一般格式如下对运算符“+”实现有复数功能的运算重载即,我们希望能进行两个复数类对象的加法操作。为了讲解达到循序渐进的目的,先看用普通成员函数的方法实现。(显式调用的方法实现)。52对运算符“+”实现有复数功能的运算重载即,我们希望能进行两个一.用显式调用成员函数的方法来实现复数类对象的加法操作classcomplex{doublerpart,ipart;public:complex(){rpart=ipart=0.0;}complex(doublerp,doubleip){rpart=rp;ipart=ip;}complexadd(constcomplex&com){complextemp;temp.rpart=com.rpart+rpart;temp.ipart=com.ipart+ip;returntemp;}};53一.用显式调用成员函数的方法来实现复数类对象的加法操作cla主调函数实现main(){complexx,y,a(10,7),b(3,5);complexz=x.add(y);complexc=a.add(b);}尽管已用函数实现了复数类对象“加”的功能,但其书写格式不自然,直观。54主调函数实现main(){complexx,y,a(10二.用关键字operator来实现classcomplex{doublerpart,ipart;public:complex(){rpart=ipart=0.0;}complex(doublerp,doubleip){rpart=rp;ipart=ip;}complexoperator+(constcomplex&com){complextemp(com.rpart+rpart,com.ipart+ipart);returntemp;}};55二.用关键字operator来实现classcomp主调函数实现main(){complexx,y,a(10,7),b(3,5);complexz=x+y;complexc=a+b;}其书写格式自然,直观。56主调函数实现main(){complexx,y,a(10三.两种方法书写格式对比理解1.complexc=a.add(b);2.complexc=a+b;3.complexc=a.operator+(b);1,3格式完全相同,以前的知识可以理解实际上,2式被编译时解释为:这种格式也是显式成员函数调用57三.两种方法书写格式对比理解1.complexc=a接上语法分析需要的重要结论---对象的单目运算符被解释为:a+ba.operator+(b)等价58接上语法分析需要的重要结论---对象的单目运算符被解释为:a四.单目运算符被重载的例子classcomplex{doublerpart,ipart;public:complex(){rpart=ipart=0.0;}complex(doublerp,doubleip){rpart=rp;ipart=ip;}complexoperator+(//..){//..}complexoperator-(){complextemp(-rpart,-ipart);returntemp;}};59四.单目运算符被重载的例子classcomplex{do接上main(){complexa(10,7,),b(3,5);complexc=-a+b;}-a被解释为a.operator-()-a+b被解释为(a.operator-()).operator+(b)问题:“-”号为什么不解释为双目的减号,而解释为单目的负号的?答案见下页:60接上main()-a被解释为a.operator-上例解释为单目负号运算符的原因C++规定,单目前缀运算符成员函数不带参数,而双目运算符成员函数必须带一个参数。本例不带参数,因此解释为单目取负运算符。61上例解释为单目负号运算符的原因C++规定,单目前缀运算符成员第四节对运算符重载的说明62第四节对运算符重载的说明62一.哪些运算符能重载不能重载:“.”“*”“::”“?”“sizeof”能重载:+-*/%^&|!|=<>+=-=*=/=%=^=&=|=<<>>>>=<<===!=<=>=&&||++--->*,->[]()newdelete等63一.哪些运算符能重载不能重载:63二.运算符重载需注意的问题(一)运算符重载只能定义运算符的含义,不能改变运算符的优先级和结合顺序。(二)运算符重载时,不能改变目数。例如把%改为单目,把!改为双目等。(三)运算符重载既可在类中定义,也可在类外定义。前面的复数类例子是在类中定义的。当在类外定义时,它至少应有一个相应类的参数。见下例:64二.运算符重载需注意的问题(一)运算符重载只能定义运算符的含例如:classX{public:inti;X(){i=1;}};//构造函数赋初值为1Xoperator+(constX&x1,constX&x2){Xtemp;temp.i=x1.i+x2.i;returntemp;}本例构造重载函数不属于任何类成员。但却用X类作为参数。这是因为重载的目的是类对象运算。没有“类”类型,怎么谈得上对象呢?65例如:classX{public:inti;本例构造接上例main(){Xx1,x2;Xx3=x1+x2;cout<<x3.i<<endl;//输出值为2}类外定义的等价形式:x3=x1+x2等价于x3=operator+(x1,x2);66接上例main()类外定义的等价形式:66

当在类外定义时,如一个相应类的参数都没有,则是错误的。如:intoperator+(intx2)//error{inttemp=x1+x2+1;returntemp;}main(){intx1=1,x2=1;intx3=x1+x2;cout<<x3<<endl;}//希望通过对“int”类型上的加法运算符进行重载。使输出结果为3。是错误的。67当在类外定义时,如一个相应类的参数都没有,则是错误的。如:(四)类内、外对运算符重载各种情况说明1.对于双目运算符@当它是类内成员函数时,只带一个参数。另一运算数由调用它的对象给出。此时aa@bb等价于aa.operator@(bb)当它是类外函数时,带有两个参数。此时aa@bb等价于operator@(aa,bb)68(四)类内、外对运算符重载各种情况说明1.对于双目运算符@6接上2.对于单目前缀运算符@当它是类内成员函数时,不必带参数。运算数就是调用它的对象。此时@bb等价于aa.operator@()当它是类外函数时,带有一个参数。此时@bb等价于operator@(aa)69接上2.对于单目前缀运算符@69接上3.对于单目后缀运算符@当它是类内成员函数时,应带一个int参数(该参数一般不会使用,只是为与前缀区分)。此时aa@等价于aa.operator@(int)当它是类外函数时,第一个是相应类的参数,第二个是int参数(该参数也一般不会使用,只是为与前缀区分)。此时aa@等价于operator@(aa,int)70接上3.对于单目后缀运算符@70(五)可同时既在类内、又在类外定义运算符函数。因是在“作用域”范围内,重载是合法的。例如:classX{public:doubleoperator+(double){//..} };doubleoperator+(X,double){//..}71(五)可同时既在类内、又在类外定义运算符函数。因是在“作用域接上main(){Xx1,x2;x2=x1+3.0;//二义性}肯定不会解释为后缀运算符。那么究竟调用的1还是2呢?本例说明类内、类外定义都是有效的。72接上main()肯定不会解释为后缀运算符。那么究竟调用的1(六)不能制造运算符如制造:乘方“**”运算符幂“x^y”运算符等是错误的。73(六)不能制造运算符如制造:73(七)C++编译器对“=”、“&”、“,”等运算符

作用于class类型上已经进行了预定义。不必重载就可直接使用其本身的运算符含义。例如前面学习继承、虚函数知识时,我们就未加说明地直接使用了象以下的代码:对象1=对象2;指针=&对象;但这三个运算符针对具体的类也可重载。74(七)C++编译器对“=”、“&”、“,”等运算符

作用于第五节重载运算符举例75第五节重载运算符举例75例一.对复数类的“+”运算符重载的进一步完善问题提出--对上例定义的运算符函数,如主调函数写为:main(){complexa,b;a=b+10.00;a=10.00+b;}解决方法:用非成员函数重载“+”运算符,并说明为complex类的友元。见下页:解释为a=b.operator+(10.00)正确解释为a=10.00.operator+(b)错误76例一.对复数类的“+”运算符重载的进一步完善问题提出--对上将非成员函数重载“+”运算符,并说明为complex类的友元complexoperator+(doubled,complex&com){complextemp(com.rpart+d,com.ipart);returntemp;}classcomplex{public://...friendcomplexoperator+(double,complex&);complexoperator+(double);//..};77将非成员函数重载“+”运算符,并说明为complex类的友以下书写就正确了main(){complexa,b;a=b+10.00;//oka=10.00+b;//ok}78以下书写就正确了main(){complexa,b;78例二complex类对赋值运算符“=”的重载comlpexa,b;a=b;C++对“=”运算符对类对象进行了预定义。其含义是一一对对象分量进行拷贝。也可将其含义定义为程序员自己需要的含义。见下页:79例二complex类对赋值运算符“=”的重载comlp有类成员为指针时,用“=”号预定义的含义无法实现“深拷贝”深拷贝----指向的内容也拷贝。因预定义的“=”运算符是一一对对象分量进行拷贝,故只能拷贝指针变量内容----即地址。这种方法可能出现数据的不一致危险错误。解决方法:对其重载,给予新的含义。80有类成员为指针时,用“=”号预定义的含义无法实现对“=”号在字符串类cmessage的重载代码(北航书)classcmessage{private:char*buffer;public:voidoperator=(constcmessage&message){delete[]buffer;buffer=newchar[strlen(message.buffer)+1];strcpy(buffer,message.buffer);}cmessage(){buffer=newchar(‘\0’);}cmessage(char*string){buffer=newchar[strlen(string)+1];}};81对“=”号在字符串类cmessage的重载代码(北航书)cl主调函数代码voidmain(){cmessagea,b(“Iamastudent.”);a=b;}Cmessage类对象赋值时实现深拷贝a=b;等价于a.operator=(b);82主调函数代码voidmai对cmessage类“=”号的进一步完善问题提出:main(){cmessagec,a,b(“Iamastudent”);c=a=b;}c=a=b;解释为c.operator=(a=b);根据=号的定义,a=b表达式的返回类型为void。要使其类型一致,其返回类型也应为cmessage83对cmessage类“=”号的进一步完善问题提出:c=a=对cmessage类“=”号函数的修改classcmessage{//…cmessage&operator=(constcmessage&message){delete[]buffer;buffer=newchar[strlen(message.buffer)+1];strcpy(buffer,message.buffer);return*this;}//…};84对cmessage类“=”号函数的修改classcmes以下就主调函数调用正确问题提出:main(){cmessagec,d,a,b(“Iamastudent”);c=d=a=b;//实现连续使用“=”号}c=d;仍然解释为c.operator=(d);d=a=b;解释为c.operator=(a=b);a=b;解释为a.operator=(b);根据=号的定义,a=b表达式的返回类型为cmessage。其类型一致,其值为对当前对象(a)的引用。cmeaaage85以下就主调函数调用正确问题提出:c=d;仍然解释为c.深入理解:根据上例的分析,可触类旁通地理解我们用“熟悉”了的,输出流运算符的下列书写格式的道理。main(){cout<<“I”<<“am”<<“student”;}深入理解:1.连续两个以上“<<”运算符重载函数。2.把cout理解为某种class类型的对象。这种类型实际上是C++预定义的“流”类。86深入理解:根据上例的分析,可触类旁通地理解我们用“熟悉”了的例三重载逻辑运算符“==”的例子对长方体对象定义判断是否相同。相等的判别为长、宽、高分别相等。classcft{int长;int宽;int高;public:intoperator==(constcft&cft1){return(长==cft1.长)&&(宽==cft1.宽)&&(高==cft1.高);}cft(intx,inty,intz){长=x;宽=y;高=z;}};87例三重载逻辑运算符“==”的例子对长方体对象定义判断是否主调函数main(){cfta(1,2,3),b(4,5,6);if(a==b)cout<<“两个对象大小一样”<<endl;elsecout<<“两个对象大小不一样”<<endl;}88主调函数main()88例四integer类型的++运算符的重载1.后缀:先用再加1classinteger{public:inti;integer(intx){i=x;}integeroperator++(int){integertemp=*this;i++;returntemp;}};先用再加1有int参数,仅起区分作用89例四integer类型的++运算符的重载1.后缀:先主调函数main(){integera(1),b(0);b=a++;cout<<b.i<<endl;//1}等价于b=a.operator++(0);调用

90主调函数main()等价于b=a.operator++(接上2.前缀:先加1再用classinteger{public:inti;integer(intx){i=x;}integeroperator++(){i++;return*this;}};无int参数,仅起区分作用先加1再用91接上2.前缀:先加1再用无int参数,仅起区分作用主调函数main(){integera(1),b(0);b=++a;cout<<b.i<<endl;//1}等价于b=a.operator();调用

92主调函数main()等价于b=a.operator();例五.对“[]”下标运算符重载下标运算符是一个双目运算符。第一个运算对象是数组名,第二个运算对象是数组下标。运算符重载函数格式:T1T::operator[]();该函数只能是类内定义。x[y]调用等价于x.operator[](y);例见下页。93例五.对“[]”下标运算符重载下标运算符是一个双目运算符。用下标运算符检查越界的错误classainteger{ int*a;intsz;public:ainteger(intsize){sz=size;a=newint[size];} int&operator[](inti) {if(i<0||i>=sz) {cout<<"error";exit(1);} elsereturna[i];} ~ainteger(){delete[]a;} };94用下标运算符检查越界的错误classainteger{ i主调函数voidmain(){aintegerai(10);ai[2]=3;cout<<ai[2]<<endl;//3ai[10]=50;cout<<ai[10]<<endl;//errorinti=ai[2];//执行3}95主调函数voidmain()95再提复制构造函数前面已谈到,如对赋值运算符“=”没有重载,C++有其预定义的含义,即逐一拷贝对象成员。假如类型为A:Aa,b;a=b;当类成员为指针时,应对“=”运算符在A类内重载,定义含义为指针所指内容也被“拷贝”。b对象内的成员被逐一拷贝到a96再提复制构造函数前面已谈到,如对赋值运算符“=”没有重载,对象初始化与对象赋值的区别对象赋值:指对象已经建立后,使用赋值运算符“=”。Aa,b,c;b=a;初始化:对象建立时,“立即”对其给定初始值。如Aa,b(10),c(10);Ad(a);Ae=a;97对象初始化与对象赋值的区别对象赋值:指对象已经建立后,使用赋C++中体现“初始化对象”的另外两个情况把对象作为参数传递时函数返回一个类对象时讲解这两种情况98C++中体现“初始化对象”的另外两个情况把对象作为参数传递时复制构造函数用于初始化类对象的上述那些场合。复制构造函数的定义:指类的构造函数为单参数,且该参数为引用此类类型。例如:classA{public:char*name;A(char*name1){//.}//普通构造函数A(constA&aa){name=newchar[strlen()+1];strcpy(name,);}};99复制构造函数用于初始化类对象的上述那些场合。复制构造函数的定主调函数main(){Aa(“student”);Ab(a);cout<<<<endl;}调用普通构造函数,因参数类型为char*1.调用复制构造函数。因参数类型为A类对象,而且该行语句表示了是在“初始化”。2.如没定义复制构造函数,这种书写格式将调用缺省的复制构造函数。缺省的复制构造函数“含义”也是逐一“拷贝”,不能进行深处理,即无法“确保”输出结果为student。输出结果为student100主调函数main()调用普通构造函数,因参数类型为char对当类中有成员为指针时的补充说明。由于是指针,应进行深处理。所以要仔细考虑以下几方面的成员函数定义的深处理。1.普通构造函数。用于如Ab(“student”);表达的目的。2.复制构造函数。用于如Aa;Ab(a);表达的目的。3.赋值运算符函数。用于如Aa,b;a=b;表达式的目的。4.析构函数。用于及时清理内存。101对当类中有成员为指针时的补充说明。由于是指针,应进行深处理。自定义类型转换102自定义类型转换102一.构造函数实现不同类的转换在基本类型的运算中,不同类型混合运算时,其类型转换是自动进行的。同样,在不同类class的生成对象中,混合运算C++也是允许的,这样将扩大重载运算符的用途。但必须在类中定义转换构造函数。显然,转换的目的是将其它类变为当前类。所以转换构造函数只能有一个其它类的参数。103一.构造函数实现不同类的转换在基本类型的运算中,不同类型混合为了清晰,下例为将基本类型double转换为复数类classcomplex{doublerpart,ipart;public:complex(){rpart=ipart=0.0;}complex(doublerp,doubleip){rpart=rp;ipart=ip;}

complex(constdouble&d){rpart=d;ipart=0;}complexoperator+(constcomplex&com){complextemp(com.rpart+rpart,com.ipart+ipart);returntemp;}};104为了清晰,下例为将基本类型double转换为复数类cla主调函数main(){complexa,b(1,4),c;c=a+b;c=a+3.0;}调用“+”运算符成员函数。a+b解释为a.operator+(b)调用“+”运算符成员函数。但调用之时还要调用转换构造函数。此时a+3.0解释为a.operator+(无名临时对象)。其中无名临时对象可以理解为这样来生成的:

complex无名临时对象(3.0);调用了转换构造函数105主调函数main()调用“+”运算符成员函数。a+b解释为说明转换构造函数实质上是一个只带单参数且该参数不是自身类的普通构造函数。但是,要达到不同书写格式的类的转换。要适当考虑如上例的一些技巧。如:main(){complexa,b;b=3.0+a;}//error。

此时,解释为3.0.operatoor(a)。应当将“+”运算符定义为外部友员。106说明转换构造函数实质上是一个只带单参数且该参数不是自身类的普用于转换的构造函数一旦定义其作用不仅是混合运算时类转换。象初始化、函数返回、对象作为形参到实参等情况都可使用。其原因是它仍然是一个普通构造函数。如:complexf(){return1.0;}voidff(complexc);main(){complexa(52.3);complexb;b=f();//赋值号右断是complex类临时对象ff(b);}初始化函数返回。即值1.0要调用转换构造函数b作为实参传给形参c107用于转换的构造函数一旦定义其作用不仅是混合运算时类转换。象初实际上,转换构造函数还能进行从任何类类型到该类的转换如:classcubed{public:intx,y,z;//..};classcomplex{doublerpart,ipart;public:complex(){rpart=ipart=0.0;}complex(doublerp,doubleip){rpart=rp;ipart=ip;}

complex(constcubed&d){rpart=d.x;ipart=d.y;}complexoperator+(constcomplex&com){complextemp(com.rpart+rpart,com.ipart+ipart);returntemp;}};cubed中的成员为公有。z不用。程序员要“心中有数”108实际上,转换构造函数还能进行从任何类类型到该类的转换如:cl主调函数main(){cubedb;complexa,c;c=a+b;}调用“+”运算符成员函数。但调用之时还要调用转换构造函数,将cubed类对象b转换为compllex类临时对象。此时a+b解释为a.operator+(无名临时对象)。其中无名临时对象可以理解为这样来生成的:

complex无名临时对象(b);调用了转换构造函数来进行从b到无名临时对象的转换109主调函数main()调用“+”运算符成员函数。但调用之时还要二.转换运算符主要实现从类对象到基本类型数据的转换。转换运算符无函数类型和无参数。classinteger{inti;public:integer(inta){i=a;}integer(){i=10;}operatorint(){returni;} };转换构造函数转换运算符函数110二.转换运算符主要实现从类对象到基本类型数据的转换。转换运算主调函数voidmain(){ integerk; intj=k;cout<<j<<endl;//?}因在“作用域”范围内需要int类型,故调用了转换运算符int函数111主调函数voidmain()因在“作用域”范围内需要in实际上,转换运算符还能进行从任何类类型到该类的转换classA{public:intx;};

classinteger{inti;

public:

integer(inta){i=a;}

integer(){i=10;}

operatorA(){Aaa;aa.x=i;returnaa;}

};A”运算符”写在integer内,说明可进行integer到A的转换。深入理解:运算符也是函数吗?112实际上,转换运算符还能进行从任何类类型到该类的转换class主调函数voidmain()

{

integerk;

Aj=k;

cout<<j.x<<endl;

}从integer类到A类的转换调用了转换运算符成员函数113主调函数voidmain()

{

integer三.与C语言对比,正确理解C++格式的强制转换inti;integerir;C格式:ir=(integer)i;//i=(int)ir;C++格式i=int(ir);ir=integer(i);调用integer类的转换运算符函数

int调用integer类的转换构造函数在C的强制转换格式中,无法将运算符理解为函数。在C++中,有明确的函数格式及其代码。114三.与C语言对比,正确理解C++格式的强制转换inti;structX{ inti; X(int); operator+(int);};structY{ inti; Y(X); operator+(X); operatorint();};在作用域内,“+”运算符有下列“类型参数”的重载函数:1.对基本类型预定义2.X+int(类内)

3.Y+X(类内)在作用域内,“*”运算符有下列“类型参数”的重载函数:1.对基本类型预定义2.X*Y(类外)在作用域内,类型转换有函数:1.int到X2.X到Y3.Y到int运算符重载例程分析115structX{ inti;在作用域内,“+”运算符有下接上Xoperator*(X,Y);intf(X);Xx=1;Yy=x;inti=2;调用转换构造函数X(int)调用转换构造函数Y(X)116接上Xoperator*(X,Y);调用转换构造函数X(voidmain(){ i+10;//OK调用预定义 y+10;//二义性

y+10*y;//二义性究竟调用下列哪一个“+”号函数呢?-对基本类型预定义,即int(通过Y到int转换而来)+int-Y+X(通过int到X转换而来)究竟调用下列哪一个“*”号函数呢?-对基本类型预定义,即int*int(通过Y到int转换而来)-X*Y[其中X通过X(int)转换而来]117voidmain()究竟调用下列哪一个“+”号函数呢?究竟接上 x+y+i//OK

x*x+i;//OK f(7);//OK f(y);//ERROR x+y;//OK 106+y;//OK}Y转换为int后,再调用X.operator+(int)int转换为X。无Y到X的转换。X转换为Y后,再调用operator*(X,Y)Y转换为int后,再调用X.operator+(int)Y转换为int后,再调用预定义的int+int118接上 x+y+i//OKY转换为int后,再调用X.op对complex重载乘“*”运算符。119对complex重载乘“*”运算符。119说明:1.多态性是OOP的重要组成部分。它是C++中的难点之一。C++的多态性是靠虚函数实现的。实际上虚函数使类的成员函数表现出多种形态。2.虚函数的形式十分简单,要领会其实质则不简单。第5章虚函数与多态性120说明:第5章虚函数与多态性1本章主要内容多态性的概念和多态类型联编虚函数抽象类运算符重载121本章主要内容多态性的概念和多态类型2多态性的概念多态性是指具有相似功能的不同函数使用同一个名称来实现,从而可以使用相同的调用方式来调用这些具有不同功能的同名函数的特性。C++的多态性是指对同一条消息,被不同类型的对象接收将产生不同的行为。多态性是实现“一种接口,多种方法”的技术。这里的“一种接口”是指相同函数名,而“多种方法”是指多种函数实现。122多态性的概念多态性是指具有相似功能的不同函数使用同一个名称联编多态性的实现过程中,确定调用哪个同名函数的过程就是联编(binding),又称绑定。联编是指计算机程序自身彼此关联的过程,也就是把一个标识符名和一个存储地址联系在一起的过程。用面向对象的术语讲,就是把一条消息和一个对象的方法相结合的过程。按照联编进行的阶段的不同,可以分为静态联编和动态联编,这两种联编过程分别对应着多态性的两种实现方式。123联编多态性的实现过程中,确定调用哪个同名函数的过程就是联编静态联编在编译阶段完成的联编称为静态联编。在编译过程中,编译系统可以根据类型匹配等特征来确定程序中调用操作与执行某个同名函数实现之间的关系,即确定某一个同名函数到底是要调用哪一段函数实现代码。函数重载和运算符重载就是通过静态联编方式实现的编译时的多态的体现。静态联编的优点是函数调用速度快、效率较高,缺点是编程不够灵活。124静态联编在编译阶段完成的联编称为静态联编。5动态联编只有在运行程序时才能确定将要调用的函数。这种在运行阶段进行的联编称为动态联编。动态联编的优点是提供了更好的编程灵活性、问题抽象性和程序易维护性,缺点是与静态联编相比,函数调用速度慢。125动态联编只有在运行程序时才能确定将要调用的函数。这种在运行虚函数虚函数是动态联编的基础。虚函数是非静态的成员函数,经过派生之后,虚函数在类族中可以实现运行时的多态性。126虚函数虚函数是动态联编的基础。7虚函数的声明虚函数是一个在某基类中声明为virtual并在一个或多个派生类中被重新定义的成员函数。声明虚函数的格式如下:virtual<返回值类型><函数名>(<参数表>);127虚函数的声明虚函数是一个在某基类中声明为virtual并在虚函数的使用如果某类中的一个成员函数被说明为虚函数,这就意味着该成员函数在派生类中可能有不同的函数实现。当使用对象指针或对象引用调用虚函数时,采用动态联编方式,即在运行时进行关联或绑定。128虚函数的使用如果某类中的一个成员函数被说明为虚函数,这就意例1classemployee{//...public:

virtualwhoami(){//…}//..};classmanager:publicemployee{//...public:

virtualwhoami(){//…}//..};129例1classemployee{//...10接上例classmanager:publicmanager{//...public:

virtualwhoami(){//…}//..};130接上例classmanager:publicmanag接上例voidmain(){employee*emp[3];emp[0]=newemployee;emp[1]=newmanager;emp[2]=newdirector;for(I=0;I<3;I++)emp[i]->whoami();//分别打印三种不同对象的whoami()函数。而不只是基类对象。}131接上例voidmain()12上述例子实现了一个whoami()函数有三种行为,即多态。这是关于多态性的语法方面的分析理解。为了进一步形象地理解多态,见下例:132上述例子实现了一个whoami()函数有三种行为,即多态例2classA{public:virtualvoidshow(){cout<<“classAshow()iscalled”<<endl;}};classB:publicA{public:voidshow(){cout<<“classBshow()iscalled”<<endl;}};133例2classA{public:virtualvoi接上例voidmain(){Ademo1,*p;Bdemo2;p=&demo1;p->show();//显示“classAshow()iscalled”p=&demo2;p->show();//显示”classBshow()iscalled”}//也表现了一个函数show()多种形态。同一样的语句,有不同的行为134接上例voidmain(){同一样的语句,有不同的行为15虚函数的语法总结说明1.虚函数的返回类型、名字、参数必须完全系统。2.只有类的成员函数才能是虚函数。这样才能与具体对象相联结,以实现不同类对象的同一函数的“多态”。3.静态成员函数不能说明为虚函数。135虚函数的语法总结说明1.虚函数的返回类型、名字、参数必须完全接上页4.虚函数不能是内联的。因为它实现了动态联编。假如在类内进行了定义,也认为它是非内联的。5.构造函数不能是虚函数。6.析构函数可以为虚函数,此时虚析构函数的名字为“~类型名”。以实现其“多态”地撤消不同类对象。136接上页4.虚函数不能是内联的。因为它实现了动态联编。假如在类接上页7.虚函数的“虚拟性”可以“继承”。即基类写了关键字virtual后,派生类的同名、同参数、同类型函数也是虚函数,可以不写virtual。多层继承时,如中间类不写虚函数,其后的派生类需要时重新定义即可。即不会出现虚拟继承的传递路径中断。8.注意虚函数与虚基类的区别。137接上页7.虚函数的“虚拟性”可以“继承”。即基类写了关键字接上页9.虚函数实现多态,只能通过指针或引用进行。因为只有这样,才能实现动态联编。10.多态可简单地理解为:同一名字多种含义。如:PLAYFOOTBALLBASKETBALLPINGPONGBALL138接上页9.虚函数实现多态,只能通过指针或引用进行。因为只有这再接上页11.派生类内可以调用基类的虚函数。但为了保持不破坏多态,一般不提倡这样做。12.虚函数的存取决定于首次定义它的基类中该函数是公有还是私有。如果是公有,派生类中仍然是公有;如果是私有,派生类中仍然是私有。13.注意虚函数与重载函数的区别。见下页139再接上页11.派生类内可以调用基类的虚函数。但为了保持不破坏虚函数与重载函数区别重载函数虚函数返回值参数不同任意静态不同同语义相关性适用范围联接名字同同动态派生类应语义相关可以语义无关同140虚函数与重载函数区别重载函数虚函数返回值参数不同任意静态不同多态形成的过程1.虚函数靠运行时动态联接来表现“多态”。即是根据不同的对象去调用不同类的虚函数,即实现了对象的多态;2.重载函数是靠参数匹配的差别,调用不同的函数。即重载函数无法实现“对象”的多态。尽管重载函数“看起来”似乎已经实现了多态。141多态形成的过程1.虚函数靠运行时动态联接来表现“多态”。即是C++提供的通过基类指针“自动”访问派生类的方法1.通过加类型标志成员来实现(C语言也能实现,这是一种常规技巧方法)2.通过虚函数(这是我们学习本章的主要目的)为了正确理解虚函数,以下先举用加标志成员的例子142C++提供的通过基类指针“自动”访问派生类的方法1.通过加类一.加标志成员方法加入的标志成员用来说明该基类指针所指对象究竟为哪一种类型对象。假设该标志成员为枚举类型(也可为其它类型)。程序见下例143一.加标志成员方法加入的标志成员用来说明该基类指针所指对象究例子enumempl_type{E,M,D};classemployee{public:empl_typetype;whoami(){//…}//...};classmanager:publicemployee{public:whoami(){//…}//...};classdirector:publicmanager{public:whoami(){//…}//...};144例子enumempl_type{E,M,D};25接上例main(){employee*emp[3];emp[0]=newemployee;//假设构造函数设置type为‘E’emp[1]=newmanager;//假设构造函数设置type为‘M’emp[2]=newdirector;//构造函数设置type为‘D’for(i=0;i<3;i++)switch(emp[i]->type){caseE:emp[i]->whoami();break;caseM:((manager*)emp[i])->whoami();break;caseD:((director*)emp[i])->whoami();break;}}基类指针强制转换为派生类指针145接上例main(){employee*emp[3];基类本例中的new说明newemployee;newmanager;newdirector;均自动调用该类的构造函数146本例中的new说明newemployee;均自纯虚函数在C++中,有一个仅为该多态机制提供一个接口而没有任何实体定义的函数,被称为纯虚函数。有时,基类的虚函数并不做任何事情。纯粹用于派生类的虚函数的定义需要,称为纯虚函数。C++规定,纯虚函数的专用书写格式为:virtual<返回值类型><函数名>(<参数表>)=0;147纯虚函数在C++中,有一个仅为该多态机制提供一个接口而没有抽象基类含有一个以上纯虚函数的类,被称为抽象基类,简称抽象类。如:classA{public:virrtualvoidprint()=0;//…};148抽象基类29特点:1.抽象基类不能生成对象。但可以定义指向它的指针。如:

温馨提示

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

评论

0/150

提交评论