《《计算机网络原理》第8章-多态性和虚函数-精选课件.pptx》由会员分享,可在线阅读,更多相关《《计算机网络原理》第8章-多态性和虚函数-精选课件.pptx(36页珍藏版)》请在taowenge.com淘文阁网|工程机械CAD图纸|机械工程制图|CAD装配图下载|SolidWorks_CaTia_CAD_UG_PROE_设计图分享下载上搜索。
1、lC+支持两种多态性,即编译时的多态性和运行时的多支持两种多态性,即编译时的多态性和运行时的多态性。编译时的多态性通过使用重载函数或模板获得,运态性。编译时的多态性通过使用重载函数或模板获得,运行时的多态性通过使用继承和虚函数来获得。行时的多态性通过使用继承和虚函数来获得。l联编是描述编译器决定在程序运行时,一个函数调用应联编是描述编译器决定在程序运行时,一个函数调用应执行哪段代码的一个术语,是实现多态性的基础。执行哪段代码的一个术语,是实现多态性的基础。第第8章多态性和虚函数章多态性和虚函数第第8章多态性和虚函数章多态性和虚函数28.1多态性多态性8.1.1静态联编中的赋值兼容性和名字支配规
2、律静态联编中的赋值兼容性和名字支配规律类的对象和调用的函数一一对应,程序编译类的对象和调用的函数一一对应,程序编译时即可确定对象调用哪一个函数,称为静态时即可确定对象调用哪一个函数,称为静态联编联编通过指针调用成员函数时:通过指针调用成员函数时:所调用成员函数为指针所属类的成员函数所调用成员函数为指针所属类的成员函数即由赋值兼容规则决定指针调用的成员函数即由赋值兼容规则决定指针调用的成员函数348.1.2动态联编的多态性动态联编的多态性要实现程序运行时决定指针所调用的函数是基类要实现程序运行时决定指针所调用的函数是基类的还是派生类的,即:动态联编。的还是派生类的,即:动态联编。可利用虚函数实现
3、动态联编可利用虚函数实现动态联编 由此可见,对象的内存地址空间中只包含数据成员,并不由此可见,对象的内存地址空间中只包含数据成员,并不存储有关成员函数的信息。这些成员函数的地址翻译过程与存储有关成员函数的信息。这些成员函数的地址翻译过程与其对象的内存地址无关。编译器只根据数据类型翻译成员函其对象的内存地址无关。编译器只根据数据类型翻译成员函数的地址并判断调用的合法性。数的地址并判断调用的合法性。virtualdoublearea()return0.0;当编译系统编译含有虚函数的类时,为它建立当编译系统编译含有虚函数的类时,为它建立一个虚函数表,表中的每一个元素都指向一个虚函一个虚函数表,表中的
4、每一个元素都指向一个虚函数的地址。此外,编译器也为类增加一个数据成员,数的地址。此外,编译器也为类增加一个数据成员,这个数据成员是一个指向该虚函数表的指针,通常这个数据成员是一个指向该虚函数表的指针,通常称为称为vptr。如果派生类如果派生类Circle没有重写这个没有重写这个area虚函数,虚函数,则派生类的虚函数表里的元素所指向的地址就是基则派生类的虚函数表里的元素所指向的地址就是基类类Point的虚函数的虚函数area的地址,即派生类仅继承基类的地址,即派生类仅继承基类的虚函数,它调用的也是基类的的虚函数,它调用的也是基类的area函数。函数。现在将它改写如下:现在将它改写如下:virt
5、ualdoublearea()returnPI*radius*radius;这时,编译器也将派生类虚函数表里的元素指向这时,编译器也将派生类虚函数表里的元素指向Circle:area(),即指向派生类,即指向派生类area函数的地址。函数的地址。由此可见,虚函数的地址翻译取决于对象的内存地址。编译由此可见,虚函数的地址翻译取决于对象的内存地址。编译器为含有虚函数类的对象首先建立一个入口地址,这个地址用器为含有虚函数类的对象首先建立一个入口地址,这个地址用来存放指向虚函数表的指针来存放指向虚函数表的指针vptr,然后按照类中虚函数的声明次,然后按照类中虚函数的声明次序,一一填入函数指针。序,一一
6、填入函数指针。当调用虚函数时,先通过当调用虚函数时,先通过vptr找到虚函数表,然后再找出找到虚函数表,然后再找出虚函数的真正地址。虚函数的真正地址。l派生类能继承基类的虚函数表,而且只要是和基派生类能继承基类的虚函数表,而且只要是和基类同名的类同名的(参数也相同参数也相同)成员函数,无论是否使用成员函数,无论是否使用virtual声明,它们都自动成为虚函数。声明,它们都自动成为虚函数。l如果派生类没有改写继承基类的虚函数,则函数如果派生类没有改写继承基类的虚函数,则函数指针调用基类的虚函数。如果派生类改写了基类指针调用基类的虚函数。如果派生类改写了基类的虚函数,编译器将重新为派生类的虚函数建
7、立的虚函数,编译器将重新为派生类的虚函数建立地址,则函数指针调用这个改写过的虚函数。地址,则函数指针调用这个改写过的虚函数。98.2虚函数虚函数8.2.1 虚函数的定义虚函数的定义虚函数的格式:虚函数的格式:虚函数的格式:虚函数的格式:virtual double area()return 0;virtual double area()return 0;虚函数的调用规则:虚函数的调用规则:虚函数的调用规则:虚函数的调用规则:根据当前对象,优先调用对象本身的虚成员函数。根据当前对象,优先调用对象本身的虚成员函数。根据当前对象,优先调用对象本身的虚成员函数。根据当前对象,优先调用对象本身的虚成员函
8、数。虚函数是动态联编的,是在执行期间接调用实际上欲联编的虚函数是动态联编的,是在执行期间接调用实际上欲联编的虚函数是动态联编的,是在执行期间接调用实际上欲联编的虚函数是动态联编的,是在执行期间接调用实际上欲联编的函数。函数。函数。函数。一旦基类定义了虚函数,该基类的派生类中的同名函数一旦基类定义了虚函数,该基类的派生类中的同名函数一旦基类定义了虚函数,该基类的派生类中的同名函数一旦基类定义了虚函数,该基类的派生类中的同名函数也自动成为虚函数。也自动成为虚函数。也自动成为虚函数。也自动成为虚函数。虚函数不能是静态成员。虚函数不能是静态成员。虚函数不能是静态成员。虚函数不能是静态成员。108.2.
9、2虚函数实现多态性虚函数实现多态性使用虚函数产生多态的个前提:使用虚函数产生多态的个前提:使用虚函数产生多态的个前提:使用虚函数产生多态的个前提:、类之间的继承关系满足赋值兼容性规则;、类之间的继承关系满足赋值兼容性规则;、类之间的继承关系满足赋值兼容性规则;、类之间的继承关系满足赋值兼容性规则;、改写了同名虚函数;、改写了同名虚函数;、改写了同名虚函数;、改写了同名虚函数;、根据赋值兼容性规则使用指针(或引用)。、根据赋值兼容性规则使用指针(或引用)。、根据赋值兼容性规则使用指针(或引用)。、根据赋值兼容性规则使用指针(或引用)。其中前提分为两种情况:其中前提分为两种情况:其中前提分为两种情
10、况:其中前提分为两种情况:、按赋值兼容性规则使用基类指针(或引用)访问虚、按赋值兼容性规则使用基类指针(或引用)访问虚、按赋值兼容性规则使用基类指针(或引用)访问虚、按赋值兼容性规则使用基类指针(或引用)访问虚函数;函数;函数;函数;、把指针(或引用)作为函数参数,即这个函数不一、把指针(或引用)作为函数参数,即这个函数不一、把指针(或引用)作为函数参数,即这个函数不一、把指针(或引用)作为函数参数,即这个函数不一定是类的成员函数,可以是普通函数,而且可以重载。定是类的成员函数,可以是普通函数,而且可以重载。定是类的成员函数,可以是普通函数,而且可以重载。定是类的成员函数,可以是普通函数,而且
11、可以重载。【P171P171中的中的中的中的displaydisplay函数函数函数函数】【例例8.2】分别使用指针和引用的分别使用指针和引用的display函数。函数。分析下面程序的输出结果:分析下面程序的输出结果:#includeusingnamespacestd;constdoublePI=3.14159;classPointprivate:doublex,y;public:Point(doublei,doublej)x=i;y=j;virtualdoublearea()return0;classCircle:publicPointprivate:doubleradius;public:
12、Circle(doublea,doubleb,doubler):Point(a,b)radius=r;doublearea()returnPI*radius*radius;voiddisplay(Point*p)coutarea()endl;voiddisplay(Point&a)couta.area()endl;voidmain()Pointa(1.5,6.7);Circlec(1.5,6.7,2.5);Point*p=&c;Point&rc=c;display(a);display(p);/display(&c)display(rc);/display(c)程序输出如下:程序输出如下:01
13、9.634919.634914.构造函数和析构函数调用虚函数构造函数和析构函数调用虚函数重要规则:采用静态联编,即只调用自己的类或基类中重要规则:采用静态联编,即只调用自己的类或基类中重要规则:采用静态联编,即只调用自己的类或基类中重要规则:采用静态联编,即只调用自己的类或基类中定义的函数,但不是任何在派生类中重定义的虚函数。定义的函数,但不是任何在派生类中重定义的虚函数。定义的函数,但不是任何在派生类中重定义的虚函数。定义的函数,但不是任何在派生类中重定义的虚函数。P83P83newnew和构造函数和构造函数和构造函数和构造函数deletedelete和析构函数和析构函数和析构函数和析构函数
14、标准标准标准标准c+c+不支持虚构造函数不支持虚构造函数不支持虚构造函数不支持虚构造函数支持虚析构函数,如果基类的虚析构函数,则派生类的支持虚析构函数,如果基类的虚析构函数,则派生类的支持虚析构函数,如果基类的虚析构函数,则派生类的支持虚析构函数,如果基类的虚析构函数,则派生类的析构函数是否用析构函数是否用析构函数是否用析构函数是否用virtualvirtual说明,均为虚析构函数;如派生说明,均为虚析构函数;如派生说明,均为虚析构函数;如派生说明,均为虚析构函数;如派生类未定义析构函数,则编译器生成的也是虚析构函数。类未定义析构函数,则编译器生成的也是虚析构函数。类未定义析构函数,则编译器生
15、成的也是虚析构函数。类未定义析构函数,则编译器生成的也是虚析构函数。在构造函数和析构函数中调用虚函数时,采在构造函数和析构函数中调用虚函数时,采用静态联编,即它们所调用的虚函数是自己的类或用静态联编,即它们所调用的虚函数是自己的类或基类中定义的函数,但不是任何在派生类中重定义基类中定义的函数,但不是任何在派生类中重定义的虚函数。的虚函数。【例例8.3】在构造函数和析构函数中调用虚函数。在构造函数和析构函数中调用虚函数。#includeusingnamespacestd;classApublic:A()virtualvoidfunc()coutConstructingAendl;A()virtu
16、alvoidfund()coutDestructorAendl;classB:publicApublic:B()func();voidfun()coutComehereandgo.;func();B()fund();classC:publicBpublic:C()voidfunc()coutClassCendl;C()fund();voidfund()coutDestructorCendl;voidmain()Cc;c.fun();输出结果如下:输出结果如下:ConstructingA/建立对象建立对象c调用调用B()产生产生Comehereandgo.ClassC/c.fun()输出输出De
17、structorC/析构对象析构对象c时,由时,由C()产生产生DestructorA/析构对象析构对象c时调用时调用B()产生产生p析构时应先调用析构时应先调用C的析构函数,输出的析构函数,输出“DestructorC”。接。接着调用类着调用类B的析构函数,这个析构函数调用虚函数的析构函数,这个析构函数调用虚函数fund。这个虚函数分别在类这个虚函数分别在类B的基类的基类A和派生类和派生类C中定义,它只能中定义,它只能调用它的基类中的虚函数调用它的基类中的虚函数fund,输出,输出“DestructorA”。基。基类类A中的析构函数没有输出信息,程序结束运行。中的析构函数没有输出信息,程序结
18、束运行。p目前推荐的目前推荐的C+标准不支持虚构造函数。由于析构函数标准不支持虚构造函数。由于析构函数不允许有参数,因此一个类只能有一个虚析构函数。虚析不允许有参数,因此一个类只能有一个虚析构函数。虚析构函数使用构函数使用virtual说明。只要基类的析构函数被说明为虚说明。只要基类的析构函数被说明为虚函数,则派生类的析构函数,无论是否使用函数,则派生类的析构函数,无论是否使用virtual进行说进行说明,都自动地成为虚函数。明,都自动地成为虚函数。pdelete运算符和析构函数一起工作(运算符和析构函数一起工作(new和构造函数一起和构造函数一起工作),当使用工作),当使用delete删除一
19、个对象时,删除一个对象时,delete隐含着对析隐含着对析构函数的一次调用,如果析构函数为虚函数,则这个调用构函数的一次调用,如果析构函数为虚函数,则这个调用采用动态联编。采用动态联编。p一般说来,如果一个类中定义了虚函数,析构函数也应一般说来,如果一个类中定义了虚函数,析构函数也应说明为虚函数,尤其是在析构函数要完成一些有意义的任说明为虚函数,尤其是在析构函数要完成一些有意义的任务时,例如释放内存等。务时,例如释放内存等。p如果基类的析构函数为虚函数,则在派生类未定义析构如果基类的析构函数为虚函数,则在派生类未定义析构函数时,编译器所生成的析构函数也为虚函数。函数时,编译器所生成的析构函数也
20、为虚函数。19可以在基类Point中加一个area函数,并声明为虚函数:virtual float area()return 0;其返回值为0,表示“点”是没有面积的。其实,在基类中并不使用这个函数,其返回值也是没有意义的。为简化,只给出函数的原型,并在后面加上“=0”,如virtual float area()=0;/纯虚函数 纯虚函数:是在声明虚函数时被“初始化”为0的函数。声明纯虚函数的一般形式是:virtual 函数类型 函数名(参数表列)=0;8.2.4 纯虚函数与抽象类纯虚函数与抽象类20l纯虚函数有时在基类中将某一成员函数定为虚函数,并不是基类本身的要求,而是考虑到派生类的需要,
21、在基类中预留了一个函数名,具体功能留给派生类根据需要去定义。如:基类Point中没有求面积的area函数;直接派生类Circle中需要有area函数求圆面积;间接派生类Cylinder中需要有area函数求圆柱体表面积。8.2.4 纯虚函数与抽象类纯虚函数与抽象类218.2.48.2.4 纯虚函数与抽象类纯虚函数与抽象类纯虚函数与抽象类纯虚函数与抽象类抽象类的派生类如果没有实现抽象类中的全部纯抽象类的派生类如果没有实现抽象类中的全部纯虚函数,这个派生类依旧是抽象类。虚函数,这个派生类依旧是抽象类。纯虚函数与空的虚函数是不同的:纯虚函数与空的虚函数是不同的:、virtual void area(
22、)=0;、virtual void area()【例例9.5】编写一个程序,用于计算正方形、矩形、编写一个程序,用于计算正方形、矩形、直角三角形和圆的总面积。直角三角形和圆的总面积。classshapepublic:virtualdoublearea()=0;/纯虚函数纯虚函数;classsquare:publicshapeprotected:doubleH;public:square(doublei)H=i;doublearea()returnH*H;classcircle:publicsquarepublic:circle(doubler):square(r)doublearea()ret
23、urnH*H*3.14159;classtriangle:publicsquareprotected:doubleW;public:triangle(doubleh,doublew):square(h)W=w;doublearea()returnH*W*0.5;classrectangle:publictrianglepublic:rectangle(doubleh,doublew):triangle(h,w)doublearea()returnH*W;doubletotal(shape*s,intn)doublesum=0.0;for(inti=0;iarea();returnsum;#in
24、cludeusingnamespacestd;voidmain()shape*s5;s0=newsquare(4);s1=newtriangle(3,6);s2=newrectangle(3,6);s3=newsquare(6);s4=newcircle(10);for(inti=0;i5;i+)coutsi=area()endl;doublesum=total(s,5);coutThetotalareais:sumendl;程序输出结果如下:程序输出结果如下:s0=16s1=9s2=18s3=36s4=314.159Thetotalareais:393.159shape类中的虚函数类中的虚函
25、数area仅起到为派生类提供一个仅起到为派生类提供一个一致的接口的作用,派生类中重定义的一致的接口的作用,派生类中重定义的area用于决定以用于决定以什么样的方式计算面积。由于在什么样的方式计算面积。由于在shape类中不能对此做类中不能对此做出决定,因此被说明为纯虚函数。出决定,因此被说明为纯虚函数。由此可见,赋值兼容规则使人们可将正方形、三角由此可见,赋值兼容规则使人们可将正方形、三角形和圆等都视为形状,多态性又保证了函数形和圆等都视为形状,多态性又保证了函数total在对各在对各种形状求面积之和时,无须关心当前正在计算哪种具体种形状求面积之和时,无须关心当前正在计算哪种具体形状的面积。在
26、需要时,函数形状的面积。在需要时,函数total可从这些形状的对象可从这些形状的对象那里获得该对象的面积,成员函数那里获得该对象的面积,成员函数area保证了这点。保证了这点。27l注意注意:1.纯虚函数没有函数体;纯虚函数没有函数体;2.最后面的最后面的“=0”并不表示函数返回值为并不表示函数返回值为0,它只起形式上的,它只起形式上的作用,告诉编译系统作用,告诉编译系统“这是纯虚函数这是纯虚函数”;3.这是一个声明语句,最后应有分号。这是一个声明语句,最后应有分号。4.纯虚函数的作用是在基类中为其派生类保留一个函数的纯虚函数的作用是在基类中为其派生类保留一个函数的名字,以便派生类根据需要对它
27、进行定义。名字,以便派生类根据需要对它进行定义。5.如果在一个类中声明了纯虚函数,而在其派生类中没有如果在一个类中声明了纯虚函数,而在其派生类中没有对该函数定义,则该虚函数在派生类中仍然为纯虚函数。对该函数定义,则该虚函数在派生类中仍然为纯虚函数。8.2.4 纯虚函数与抽象类纯虚函数与抽象类28抽象类:如果声明了一个类,它们不用来生成对象。定义这些类的惟一目的是用它作为基类去建立派生类。它们作为一种基本类型提供给用户,用户在这个基础上根据自己的需要定义出功能各异的派生类。用这些派生类去建立对象。这种不用来定义对象而只作为一种基本类型用作继承的类,称为抽象类(abstract class),由于
28、它常用作基类,通常称为抽象基类(abstract base class)。8.2.4 纯虚函数与抽象类纯虚函数与抽象类29凡是包含纯虚函数的类都是抽象类。因为纯虚函数是不能被调用的,包含纯虚函数的类是无法建立对象的。抽象类的作用是作为一个类族的共同基类,或者说,为一个类族提供一个公共接口。一个类层次结构中当然也可不包含任何抽象类,每一层次的类都是实际可用的,可以用来建立对象的。但是,许多好的面向对象的系统,其层次结构的顶部是一个抽象类,甚至顶部有好几层都是抽象类。8.2.4 纯虚函数与抽象类纯虚函数与抽象类30如果在抽象类所派生出的新类中对基类的所有纯虚函数进行了定义,那么这些函数就被赋予了功
29、能,可以被调用,这个派生类就不是抽象类,可以用来创建对象。如果在派生类中没有对所有纯虚函数进行定义,则此派生类仍然是抽象类,不能用来定义对象。虽然抽象类不能定义对象(或者说抽象类不能实例化),但可以定义指向抽象类数据的指针变量。当派生类成为具体类之后,就可以用这种指针指向派生类对象,然后通过该指针调用虚函数,实现多态性的操作。8.2.4 纯虚函数与抽象类纯虚函数与抽象类31l#include lusing namespace std;lclass B0 /抽象基类抽象基类B0声明声明l public:/外部接口外部接口virtual void display()=0;/纯虚函数成员l;lcla
30、ss B1:public B0 /公有派生公有派生l public:void display()coutB1:display()endl;/虚成员函数l;lclass D1:public B1 /公有派生公有派生l public:void display()coutD1:display()display();lint main()/主函数主函数lB0*p;/声明抽象基类指针声明抽象基类指针lB1 b1;/声明派生类对象声明派生类对象lD1 d1;/声明派生类对象声明派生类对象lp=&b1;lfun(p);/调用派生类调用派生类B1函数成员函数成员lp=&d1;lfun(p);/调用派生类调用派
31、生类D1函数成员函数成员l运行结果:运行结果:B1:display()D1:display()338.3多重继承与虚函数(识记)多重继承与虚函数(识记)多重继承是多个单一继承的集合。多重继承是多个单一继承的集合。p176 在派生类中,当一个指向基类成员函数的指针指在派生类中,当一个指向基类成员函数的指针指向一个虚函数,并且通过指向对象的基类指针向一个虚函数,并且通过指向对象的基类指针(或引用)访问这个虚函数时,会发生多态性。(或引用)访问这个虚函数时,会发生多态性。p176 多重继承可以被视为多个单一继承的组合,因此,多重继承可以被视为多个单一继承的组合,因此,分析多重继承情况下的虚函数调用与
32、分析单一继承分析多重继承情况下的虚函数调用与分析单一继承有相似之处。例如设计下面的类:有相似之处。例如设计下面的类:【例例9.7】多重继承使用虚函数。多重继承使用虚函数。#includeusingnamespacestd;classApublic:virtualvoidf()coutCallAendl;classBpublic:voidf()coutCallBendl;classC:publicA,publicBpublic:voidf()coutCallCf();/输出输出CallCpb-f();/输出输出CallCpc-f();/输出输出CallC368.4类成员函数的指针与多态性(识记)类成员函数的指针与多态性(识记)在派生类中,当一个指向基类成员函数的指针指在派生类中,当一个指向基类成员函数的指针指向一个虚函数,并且通过指向对象的基类指针向一个虚函数,并且通过指向对象的基类指针(或引用)访问这个虚函数时,会发生多态性。(或引用)访问这个虚函数时,会发生多态性。p176