1、第第12章章 多态性与虚函数多态性与虚函数12.1 多态性的概念多态性的概念12.2 一个典型的例子一个典型的例子12.3 虚函数虚函数12.4 纯虚函数与抽象类纯虚函数与抽象类多态性多态性(polymorphism)是面向对象程序设计的一个重是面向对象程序设计的一个重要特征。要特征。利用多态性可以设计和实现一个易于扩展的利用多态性可以设计和实现一个易于扩展的系统。系统。在在C+程序设计中,程序设计中,多态性是指具有不同功能的函数多态性是指具有不同功能的函数可以用可以用同一个函数名同一个函数名,这样就可以,这样就可以用一个函数名调用用一个函数名调用不同内容的函数不同内容的函数。在面向对象方法中
2、的多态性在面向对象方法中的多态性:向不同的对象发送同一向不同的对象发送同一个消息(函数名),不同的对象在接收时会产生不同个消息(函数名),不同的对象在接收时会产生不同的行为的行为(即方法即方法)。也就是说,也就是说,每个对象可以用自己的每个对象可以用自己的方式去响应共同的消息。方式去响应共同的消息。12.1 多态性的概念多态性的概念优点:优点:在在C+程序设计中,在不同的类中定义了其响程序设计中,在不同的类中定义了其响应消息的方法,那么使用这些类时,不必考虑它们是应消息的方法,那么使用这些类时,不必考虑它们是什么类型,只要发布消息即可。什么类型,只要发布消息即可。多态性分为两类多态性分为两类:
3、静态多态性和动态多态性。静态多态性和动态多态性。静态多态性:静态多态性:函数重载和运算符重载属于静态多态性,函数重载和运算符重载属于静态多态性,在程序编译时系统就能决定调用的是哪个函数,因此在程序编译时系统就能决定调用的是哪个函数,因此静态多态性又称编译时的多态性。静态多态性又称编译时的多态性。动态多态性:动态多态性:是在程序运行过程中才动态地确定操作是在程序运行过程中才动态地确定操作所针对的对象。它又称运行时的多态性。所针对的对象。它又称运行时的多态性。动态多态性动态多态性是通过是通过虚函数虚函数(virtual function)实现的。实现的。在本章中主要介绍动态多态性和虚函数。在本章中
4、主要介绍动态多态性和虚函数。下面是一个承上启下的例子。一方面它是有关继承和下面是一个承上启下的例子。一方面它是有关继承和运算符重载内容的综合应用的例子,通过这个例子可运算符重载内容的综合应用的例子,通过这个例子可以进一步融会贯通前面所学的内容,另一方面又是作以进一步融会贯通前面所学的内容,另一方面又是作为讨论多态性的一个基础用例。为讨论多态性的一个基础用例。12.2 一个典型的例子一个典型的例子例例12.1 先建立一个先建立一个Point(点点)类,包含数据成员类,包含数据成员x,y(坐坐标点标点)。以它为基类,派生出一个。以它为基类,派生出一个Circle(圆圆)类,增加类,增加数据成员数据
5、成员r(半径半径),再以,再以Circle类为直接基类,派生出类为直接基类,派生出一个一个Cylinder(圆柱体圆柱体)类,再增加数据成员类,再增加数据成员h(高高)。要。要求编写程序,重载运算符求编写程序,重载运算符“”,使之能,使之能用于输出以上类对象。用于输出以上类对象。对于一个比较大的程序,应当分成若干步骤进行。对于一个比较大的程序,应当分成若干步骤进行。先先声明基类,再声明派生类,逐级进行,分步调试声明基类,再声明派生类,逐级进行,分步调试。(1)声明基类声明基类Point类类 写出声明基类写出声明基类Point的部分的部分:#include/声明类声明类Point/声明一个类包括
6、什么?声明一个类包括什么?class Pointpublic:Point(float x=0,float y=0);/有默认参数的构造函数有默认参数的构造函数 void setPoint(float,float);/设置坐标值设置坐标值 float getX()const return x;/读读x坐标坐标 float getY()const return y;/读读y坐标坐标 friend ostream&operator(ostream&,const Point&);/重载运算符重载运算符“”protected:/受保护成员受保护成员 float x,y;/下面定义下面定义Point类的成
7、员函数类的成员函数/Point的构造函数的构造函数Point:Point(float a,float b)/对对x,y初始化初始化x=a;y=b;/设置设置x和和y的坐标值的坐标值void Point:setPoint(float a,float b)/为为x,y赋新值赋新值x=a;y=b;/重载运算符重载运算符“”,使之能输出点的坐标,使之能输出点的坐标ostream&operator(ostream&output,const Point&p)outputp.x,p.yendl;return output;以上完成了基类以上完成了基类Point类的声明。类的声明。现在要对上面写的基类声明进行
8、调试,检查它是否有现在要对上面写的基类声明进行调试,检查它是否有错,为此要错,为此要写出写出main函数。实际上它是一个测试程函数。实际上它是一个测试程序。序。int main()Point p(3.5,6.4);/建立建立Point类对象类对象p coutx=p.getX(),y=p.getY()endl;/输出输出p的坐标值的坐标值 p.setPoint(8.5,6.8);/重新设置重新设置p的坐标值的坐标值 coutp(new):pendl;/用重载运算符用重载运算符“”输出输出p点点坐标坐标程序编译通过,运行结果为程序编译通过,运行结果为x=3.5,y=6.4p(new):8.5,6.
9、8测试程序测试程序检查了基类中各函数的功能,以及运算符重检查了基类中各函数的功能,以及运算符重载的作用,证明程序是正确的。载的作用,证明程序是正确的。(2)声明派生类声明派生类Circleclass Circle:public Point/circle是是Point类的公用派生类类的公用派生类public:Circle(float x=0,float y=0,float r=0);/构造函数构造函数 void setRadius(float);/设置半径值设置半径值 float getRadius()const;/读取半径值读取半径值 float area()const;/计算圆面积计算圆面积
10、 friend ostream&operator(ostream&,const Circle&);/重载运算符重载运算符“”private:/?protected:float radius;/定义构造函数,对圆心坐标和半径初始化定义构造函数,对圆心坐标和半径初始化Circle:Circle(float a,float b,float r):Point(a,b),radius(r)void Circle:setRadius(float r)/设置半径值设置半径值radius=r;float Circle:getRadius()const return radius;/读取半径值读取半径值floa
11、t Circle:area()constreturn 3.14159*radius*radius;/重载运算符重载运算符“”,使之按规定的形式输出圆的信息,使之按规定的形式输出圆的信息ostream&operator(ostream&output,const Circle&c)outputCenter=c.x,c.y,r=c.radius,area=c.area()endl;return output;为了测试为了测试Circle类的定义,可以写出下面的主函数类的定义,可以写出下面的主函数:int main()Circle c(3.5,6.4,5.2);/建立建立Circle类对象类对象c,并
12、给定圆心坐标和半径并给定圆心坐标和半径 coutoriginal circle:nx=c.getX(),y=c.getY(),r=c.getRadius(),area=c.area()endl;/输出圆心坐标、半径和面积输出圆心坐标、半径和面积 c.setRadius(7.5);/设置半径值设置半径值 c.setPoint(5,5);/设置圆心坐标值设置圆心坐标值x,y coutnew circle:nc;/用重载运算符用重载运算符“”输出圆对象的信息输出圆对象的信息 Point&pRef=c;/pRef是是Point类的引用变量,被类的引用变量,被c初始化初始化 coutpRef:pRef;
13、/输出输出pRef的信息的信息 return 0;程序编译通过,运行结果为程序编译通过,运行结果为original circle:(输出原来的圆的数据输出原来的圆的数据)x=3.5,y=6.4,r=5.2,area=84.9486new circle:(输出修改后的圆的数据输出修改后的圆的数据)Center=5,5,r=7.5,area=176.714pRef:5,5 (输出圆的圆心输出圆的圆心“点点”的数据的数据)(3)声明声明Circle的派生类的派生类Cylinderclass Cylinder:public Circle/Cylinder是是Circle的公用派生类的公用派生类publ
14、ic:Cylinder(float x=0,float y=0,float r=0,float h=0);/构造函数构造函数 void setHeight(float);/设置圆柱高设置圆柱高 float getHeight()const;/读取圆柱高读取圆柱高 float area()const;/计算圆表面积计算圆表面积 float volume()const;/计算圆柱体积计算圆柱体积 friend ostream&operator(ostream&,const Cylinder&);/重载运算符重载运算符“”protected:float height;/圆柱高圆柱高;Cylinder
15、:Cylinder(float a,float b,float r,float h)/定义构造函数定义构造函数 :Circle(a,b,r),height(h)void Cylinder:setHeight(float h)height=h;/设置圆柱高设置圆柱高float Cylinder:getHeight()const return height;/读取圆柱高读取圆柱高/计算圆表面积计算圆表面积float Cylinder:area()const return 2*Circle:area()+2*3.14159*radius*height;float Cylinder:volume()c
16、onst/计算圆柱体积计算圆柱体积return Circle:area()*height;/重载运算符重载运算符“”ostream&operator(ostream&output,const Cylinder&cy)outputCenter=cy.x,cy.y,r=cy.radius,h=cy.height narea=cy.area(),volume=cy.volume()endl;return output;可以写出下面的主函数可以写出下面的主函数:int main()Cylinder cy1(3.5,6.4,5.2,10);/定义定义Cylinder类对象类对象cy1 coutn ori
17、ginal cylinder:nx=cy1.getX(),y=cy1.getY(),r=cy1.getRadius(),h=cy1.getHeight()narea=cy1.area(),volume=cy1.volume()endl;/用系统定义的运算符用系统定义的运算符“”输出输出cy1的数据的数据 cy1.setHeight(15);/设置圆柱高设置圆柱高 cy1.setRadius(7.5);/设置圆半径设置圆半径 cy1.setPoint(5,5);/设置圆心坐标值设置圆心坐标值x,y coutnnew cylinder:ncy1;/用重载运算符用重载运算符“”输出输出cy1的数据的
18、数据 Point&pRef=cy1;/pRef是是Point类对象的引用变量类对象的引用变量 coutnpRef as a Point:pRef;/pRef作为一个作为一个“点点”输出输出 Circle&cRef=cy1;/cRef是是Circle类对象的引用变量类对象的引用变量 coutncRef as a Circle:display();”可以调用不同派生层次中可以调用不同派生层次中的的display函数,函数,只需在调用前给指针变量只需在调用前给指针变量pt赋以不同赋以不同的值的值(使之指向不同的类对象使之指向不同的类对象)即可。即可。C+C+中的虚函数就是用来解决这个问题的。中的虚函
19、数就是用来解决这个问题的。虚函数的作用虚函数的作用是允许在派生类中重新定义与基类同名是允许在派生类中重新定义与基类同名的函数,并且可以的函数,并且可以通过基类指针或引用通过基类指针或引用来访问基类和来访问基类和派生类中的同名函数。派生类中的同名函数。方法:由基类指针方法:由基类指针,访问各层次中的同名函数。访问各层次中的同名函数。例例12.2。再讨论使用虚函数的情况。再讨论使用虚函数的情况。例例12.2 基类与派生类中有同名函数。基类与派生类中有同名函数。在下面的程序中在下面的程序中Student是基类,是基类,Graduate是派生类,是派生类,它们都有它们都有display这个同名的函数。
20、这个同名的函数。#include#include using namespace std;/声明基类声明基类Studentclass Studentpublic:Student(int,string,float);/声明构造函数声明构造函数 void display();/声明输出函数声明输出函数 protected:/受保护成员,派生类可以访问受保护成员,派生类可以访问 int num;string name;float score;/Student类成员函数的实现类成员函数的实现Student:Student(int n,string nam,float s)/定义构造函数定义构造函数 n
21、um=n;name=nam;score=s;void Student:display()/定义输出函数定义输出函数coutnum:numnname:namenscore:scorenn;/声明公用派生类声明公用派生类Graduateclass Graduate:public Studentpublic:Graduate(int,string,float,float);/声明构造函数声明构造函数 void display();/声明输出函数声明输出函数private:float pay;/Graduate类成员函数的实现类成员函数的实现void Graduate:display()/定义输出函数
22、定义输出函数 coutnum:numnname:namenscore:scorenpay=paydisplay();pt=&grad1;pt-display();return 0;运行结果如下,请仔细分析。运行结果如下,请仔细分析。num:1001(stud1的数据的数据)name:Liscore:87.5num:2001 (仅输出了仅输出了grad1中基类部分的数据,没实现多态性!中基类部分的数据,没实现多态性!)name:wangscore:98.5下面对程序作一点修改,在下面对程序作一点修改,在Student类中声明类中声明display函数时,函数时,在最左面加一个关键字在最左面加一个
23、关键字virtual,即即virtual void display();变成虚函数就行了变成虚函数就行了这样就把这样就把Student类的类的display函数声明为虚函数。程函数声明为虚函数。程序其他部分都不改动。再编译和运行程序,请注意分序其他部分都不改动。再编译和运行程序,请注意分析运行结果析运行结果:num:1001(stud1的数据的数据)name:Liscore:87.5num:2001 (grad1中基类部分的数据,达到目的!中基类部分的数据,达到目的!)name:wangscore:98.5pay=1200 (这一项以前是没有的这一项以前是没有的)由虚函数实现的由虚函数实现的动
24、态多态性动态多态性就是就是:同一类族中不同类同一类族中不同类的对象,对同一函数调用作出不同的响应的对象,对同一函数调用作出不同的响应。多态性理解:起始地址相同的不同对象指针,调同名多态性理解:起始地址相同的不同对象指针,调同名函数时,响应不同(调了各自对应的函数)函数时,响应不同(调了各自对应的函数)。实现方法:将同名函数声明为虚函数。实现方法:将同名函数声明为虚函数。虚函数的使用方法是虚函数的使用方法是:注意规则!注意规则!(1)在基类用在基类用virtual声明成员函数为虚函数。这样声明成员函数为虚函数。这样就可以在派生类中重新定义此函数,为它赋予就可以在派生类中重新定义此函数,为它赋予新
25、的功能,并能方便地被调用。新的功能,并能方便地被调用。在类外定义虚函数时,不必再加在类外定义虚函数时,不必再加virtual。(2)在派生类中重新定义此函数,要求函数名、函在派生类中重新定义此函数,要求函数名、函数类型、函数参数个数和类型数类型、函数参数个数和类型全部与基类的虚全部与基类的虚函数相同函数相同,并根据派生类的需要重新定义函数,并根据派生类的需要重新定义函数体。体。C+规定,规定,当一个成员函数被声明为虚函数后,其派当一个成员函数被声明为虚函数后,其派生类中的同名函数都自动成为虚函数。生类中的同名函数都自动成为虚函数。因此在派生类因此在派生类重新声明该虚函数时,可以加重新声明该虚函
26、数时,可以加virtual,也可以不加,也可以不加,但但习惯上一般在每一层声明该函数时都加习惯上一般在每一层声明该函数时都加virtual,使使程序更加清晰。程序更加清晰。如果在派生类中没有对基类的虚函数重新定义,则派如果在派生类中没有对基类的虚函数重新定义,则派生类简单地继承其直接基类的虚函数。生类简单地继承其直接基类的虚函数。(3)必须定义一个指向基类对象的指针变量必须定义一个指向基类对象的指针变量,并使它并使它指向同一类族中需要调用该函数的对象。指向同一类族中需要调用该函数的对象。(4)通过该指针变量调用此虚函数通过该指针变量调用此虚函数,此时调用的就是,此时调用的就是指针变量指向的对象
27、的同名函数。指针变量指向的对象的同名函数。举例举例2:用引用也可,如将上面例题改为:用引用也可,如将上面例题改为void fun(Student&x)/必须为基类指针或引用!必须为基类指针或引用!x.display();/主函数主函数int main()Student s1(1001,Li,87.5);Graduate gd1(2001,Wang,98.5,563.5);fun(s1);fun(gd1);return 0;显示结果:显示结果:num:1001(stud1的数据的数据)name:Liscore:87.5num:2001 (grad1的数据的数据)name:wangscore:98
28、.5pay=1200注意区别:注意区别:函数重载处理的是同一层次上的同名函数问题,而虚函数重载处理的是同一层次上的同名函数问题,而虚函数处理的是不同派生层次上的同名函数问题,前者函数处理的是不同派生层次上的同名函数问题,前者是横向重载,后者可以理解为纵向重载。是横向重载,后者可以理解为纵向重载。但与重载不同的是但与重载不同的是:同一类族的虚函数的首部是相同同一类族的虚函数的首部是相同的,而函数重载时函数的首部是不同的的,而函数重载时函数的首部是不同的(参数个数或参数个数或类型不同类型不同)。这样编译系统在对程序进行编译时,即能确定调用的这样编译系统在对程序进行编译时,即能确定调用的是哪个类对象
29、中的函数。是哪个类对象中的函数。确定调用的具体对象的过程称为确定调用的具体对象的过程称为关联关联(binding)。在这在这里是里是指把一个函数名与一个类对象捆绑在一起,建立指把一个函数名与一个类对象捆绑在一起,建立关联。关联。一般地说,关联指把一个标识符和一个存储地一般地说,关联指把一个标识符和一个存储地址联系起来。址联系起来。12.3.2 静态关联与动态关联静态关联与动态关联 函数重载和通过对象名调用的虚函数,在编译时即可函数重载和通过对象名调用的虚函数,在编译时即可确定其调用的虚函数属于哪一个类,确定其调用的虚函数属于哪一个类,其过程称为静态其过程称为静态关联关联(static bind
30、ing),由于是在运行前进行关联的,由于是在运行前进行关联的,故又称为早期关联故又称为早期关联(early binding)。函数重载属静态函数重载属静态关联。关联。而运行时的多态性,编译系统在编译该行时是无法确而运行时的多态性,编译系统在编译该行时是无法确定调用哪一个类对象的虚函数的。定调用哪一个类对象的虚函数的。因为编译只作静态因为编译只作静态的语法检查,光从语句形式是无法确定调用对象的。的语法检查,光从语句形式是无法确定调用对象的。例如,先使例如,先使pt指向指向grad1,再执行再执行“pt-display()”,当然是调用当然是调用grad1中的中的display函数。函数。由于是在
31、运行阶由于是在运行阶段把虚函数和类对象段把虚函数和类对象“绑定绑定”在一起的,因此,此过在一起的,因此,此过程称为动态关联程称为动态关联(dynamic binding)。这种多态性是动这种多态性是动态的多态性,即运行阶段的多态性。态的多态性,即运行阶段的多态性。在运行阶段,指针可以先后指向不同的类对象,从而在运行阶段,指针可以先后指向不同的类对象,从而调用同一类族中不同类的虚函数。由于动态关联是在调用同一类族中不同类的虚函数。由于动态关联是在编译以后的运行阶段进行的,因此也称为编译以后的运行阶段进行的,因此也称为滞后关联滞后关联(late binding)。使用虚函数时,有使用虚函数时,有两
32、点要注意两点要注意:(1)只能用只能用virtual声明类的声明类的成员函数成员函数,使它成为虚函,使它成为虚函数,而不能将类外的普通函数声明为虚函数。因为数,而不能将类外的普通函数声明为虚函数。因为虚虚函数的作用:函数的作用:是允许在派生类中对基类的虚函数重新是允许在派生类中对基类的虚函数重新定义。显然,它只能用于类的继承层次结构中。定义。显然,它只能用于类的继承层次结构中。12.3.3 在什么情况下应当声明虚函数在什么情况下应当声明虚函数(2)应考虑对成员函数的调用是通过对象名还是通过应考虑对成员函数的调用是通过对象名还是通过基类指针或引用去访问,基类指针或引用去访问,如果是通过基类指针或
33、引用如果是通过基类指针或引用去访问的,则应当声明为虚函数去访问的,则应当声明为虚函数。需要说明的是需要说明的是:使用虚函数,系统要有一定的使用虚函数,系统要有一定的空间开空间开销销。当一个类带有虚函数时,编译系统会为该类构造。当一个类带有虚函数时,编译系统会为该类构造一个虚函数表一个虚函数表(virtual function table,简称简称vtable),它是一个指针数组,存放每个虚函数的入口地址。它是一个指针数组,存放每个虚函数的入口地址。系系统在进行动态关联时的统在进行动态关联时的时间开销时间开销是很少的,因此,是很少的,因此,多多态性是高效的。态性是高效的。多态性进一步理解:多态性
34、进一步理解:多态性是指在运行时,能根据其类型确认调用哪个函数的多态性是指在运行时,能根据其类型确认调用哪个函数的能力。能力。只支持类而不支持多态,称基于对象的;如只支持类而不支持多态,称基于对象的;如VB。只有支持多态,才成为面向对象的。只有支持多态,才成为面向对象的。好处分析:好处分析:如计算学生的学费,有大学生,研究生,博士如计算学生的学费,有大学生,研究生,博士生等。从程序设计看,用采取继承方式设计。希望只有生等。从程序设计看,用采取继承方式设计。希望只有一个收费员(函数)可以收各种学生的费用。一个收费员(函数)可以收各种学生的费用。而不是首而不是首先有一个管理者判断是什么学生?再分派给
35、各个类型的先有一个管理者判断是什么学生?再分派给各个类型的收费员(函数)去收费。收费员(函数)去收费。希望只有一个收费员希望只有一个收费员void fn(Student&x)x.calcTuition();若不这样,若不这样,fn中要判断,分别调用,维护量大,面向对中要判断,分别调用,维护量大,面向对象优越性被限制,象优越性被限制,又回到面向过程又回到面向过程。析构函数的作用是在对象撤销之前做必要的析构函数的作用是在对象撤销之前做必要的“清理现清理现场场”的工作。当派生类的对象从内存中撤销时一般先的工作。当派生类的对象从内存中撤销时一般先调用派生类的析构函数,然后再调用基类的析构函数。调用派生
36、类的析构函数,然后再调用基类的析构函数。但用但用delete 删除对象时存在如下问题,即没有达到期删除对象时存在如下问题,即没有达到期望的目标。如何解决?望的目标。如何解决?12.3.4 虚析构函数虚析构函数(还有什么函数可以虚之?)(还有什么函数可以虚之?)例例12.3 基类中有非虚析构函数时的执行情况。基类中有非虚析构函数时的执行情况。#include using namespace std;class Point/定义基类定义基类Point类类public:Point()/Point类构造函数类构造函数 Point()coutexecuting Point destructorendl;
37、/Point类析构函数类析构函数;class Circle:public Point /定义派生类定义派生类Circle类类public:Circle()/Circle类构造函数类构造函数 Circle()coutexecuting Circle destructorendl;/Circle类析构函类析构函数数 private:int radius;int main()Point*p=new Circle;/用用new开辟动态存储开辟动态存储空间空间delete p;/用用delete释放动态存储空间释放动态存储空间return 0;这只是一个示意的程序。这只是一个示意的程序。p是指向基类的指
38、针变量,指向是指向基类的指针变量,指向new开辟的动态存储空间,希望用开辟的动态存储空间,希望用detele释放释放p所指向的空间,即所指向的空间,即元类对象的空间元类对象的空间。但运行结果为。但运行结果为executing Point destructor表示只执行了基类表示只执行了基类Point的析构函数,的析构函数,而没有执行派生类而没有执行派生类Circle的析构函数。的析构函数。原因是:如果希望能执行派生类原因是:如果希望能执行派生类Circle的析构函数,的析构函数,方法:方法:可以将基类的析构函数声明为可以将基类的析构函数声明为虚析构函数虚析构函数,如,如virtual Poin
39、t()coutexecuting Point destructorendl;程序其他部分不改动,再运行程序,结果为程序其他部分不改动,再运行程序,结果为executing Circle destructorexecuting Point destructor先调用了派生类的析构函数,再调用了基类的析构函先调用了派生类的析构函数,再调用了基类的析构函数,符合人们的愿望。数,符合人们的愿望。特点与好处:特点与好处:如果将基类的析构函数声明为虚函数时,由该基类所如果将基类的析构函数声明为虚函数时,由该基类所派生的所有派生类的析构函数也都自动成为虚函数。派生的所有派生类的析构函数也都自动成为虚函数。这
40、样,如果程序中显式地用了这样,如果程序中显式地用了delete运算符准备删除运算符准备删除一个对象,而一个对象,而delete运算符的操作对象用了指向派生运算符的操作对象用了指向派生类对象的基类指针类对象的基类指针,则系统会调用相应类的析构函数。,则系统会调用相应类的析构函数。一般即使基类并不需要析构函数,也一般即使基类并不需要析构函数,也显式地定义一个显式地定义一个函数体为空的虚析构函数函数体为空的虚析构函数,以保证在撤销动态分配空,以保证在撤销动态分配空间时能得到正确的处理。间时能得到正确的处理。构造函数不能声明为虚函数构造函数不能声明为虚函数。这是因为在执行构造函。这是因为在执行构造函数
41、时类对象还未完成建立过程,当然谈不上函数与类数时类对象还未完成建立过程,当然谈不上函数与类对象的绑定。对象的绑定。例如在本章的例例如在本章的例12.1程序中,基类程序中,基类Point中没有求面中没有求面积的积的area函数,因为函数,因为“点点”是没有面积的,也就是说,是没有面积的,也就是说,基类本身不需要这个函数,所以在例基类本身不需要这个函数,所以在例12.1程序中的程序中的Point类中没有定义类中没有定义area函数。但是,在其直接派生函数。但是,在其直接派生类类Circle和间接派生类和间接派生类Cylinder中都需要有中都需要有area函数,函数,而且这两个而且这两个area函
42、数的功能不同,一个是求圆面积,函数的功能不同,一个是求圆面积,一个是求圆柱体表面积。一个是求圆柱体表面积。仅供派生而无实际意义的函数,故纯虚之仅供派生而无实际意义的函数,故纯虚之。12.4 纯虚函数与抽象类纯虚函数与抽象类 12.4.1 纯虚函数纯虚函数有的读者自然会想到,在这种情况下应当将有的读者自然会想到,在这种情况下应当将area声明声明为虚函数。可以在基类为虚函数。可以在基类Point中加一个中加一个area函数,并函数,并声明为虚函数声明为虚函数:virtual float area()const return 0;其返回值为其返回值为0,表示,表示“点点”是没有面积的。为简化,是没
43、有面积的。为简化,可以不写出这种无意义的函数体,只给出函数的原型,可以不写出这种无意义的函数体,只给出函数的原型,并在后面加上并在后面加上“=0”,如,如virtual float area()const=0;/纯虚函数纯虚函数这就将这就将area声明为一个纯虚函数声明为一个纯虚函数(pure virtual function)。纯虚函数纯虚函数是在声明虚函数时被是在声明虚函数时被“初始化初始化”为为0的函数。的函数。声明纯虚函数的一般形式是声明纯虚函数的一般形式是virtual 函数类型函数类型 函数名函数名(参数表列参数表列)=0;注意注意:纯虚函数没有函数体;最后面的纯虚函数没有函数体;
44、最后面的“=0”并并不表示函数返回值为不表示函数返回值为0,告诉编译系统,告诉编译系统“这是纯虚函这是纯虚函数数”;这是一个声明语句,最后应有分号。这是一个声明语句,最后应有分号。纯虚函数的作用:纯虚函数的作用:纯虚函数只有函数的名字而不具备函数的功能。它只纯虚函数只有函数的名字而不具备函数的功能。它只是通知编译系统是通知编译系统:“在这里声明一个虚函数,留待派在这里声明一个虚函数,留待派生类中定义生类中定义”。纯虚函数的作用是在基类中为其派生纯虚函数的作用是在基类中为其派生类保留一个函数的名字,以便派生类根据需要对它进类保留一个函数的名字,以便派生类根据需要对它进行定义。如果在基类中没有保留
45、函数名字,行定义。如果在基类中没有保留函数名字,则无法实则无法实现多态性。现多态性。如果在一个类中声明了纯虚函数,而在其派生类中没如果在一个类中声明了纯虚函数,而在其派生类中没有对该函数定义,则该虚函数在派生类中仍然为纯虚有对该函数定义,则该虚函数在派生类中仍然为纯虚函数。函数。如果声明了一个类,一般可以用它定义对象。但是在如果声明了一个类,一般可以用它定义对象。但是在面向对象程序设计中,往往有一些类,它们面向对象程序设计中,往往有一些类,它们不用来生不用来生成对象。定义这些类的惟一目的是用它作为基类去建成对象。定义这些类的惟一目的是用它作为基类去建立派生类。立派生类。这种不用来定义对象而只作
46、为一种基本类型用作继承这种不用来定义对象而只作为一种基本类型用作继承的类,的类,称为称为抽象类抽象类(abstract class)abstract class),由于它常用由于它常用作基类,通常称为作基类,通常称为抽象基类抽象基类(abstract base class)。12.4.2 抽象类抽象类凡是包含纯虚函数的类都是抽象类。凡是包含纯虚函数的类都是抽象类。因为纯虚函数是因为纯虚函数是不能被调用的,包含不能被调用的,包含纯虚函数的类是无法建立对象的纯虚函数的类是无法建立对象的。抽象类的作用是作为一个类族的共同基类,或者说,抽象类的作用是作为一个类族的共同基类,或者说,为一个类族提供一个公
47、共接口。为一个类族提供一个公共接口。抽象类的条件:包含纯虚函数抽象类的条件:包含纯虚函数使用规则:使用规则:如果在抽象类所派生出的新类中对基类的所有纯虚函如果在抽象类所派生出的新类中对基类的所有纯虚函数进行了定义,那么这些函数就被赋予了功能,可以数进行了定义,那么这些函数就被赋予了功能,可以被调用。这个派生类就不是抽象类,而是可以用来定被调用。这个派生类就不是抽象类,而是可以用来定义对象的具体类义对象的具体类(concrete class)。如果在派生类中没如果在派生类中没有对所有纯虚函数进行定义,则此派生类仍然是抽象有对所有纯虚函数进行定义,则此派生类仍然是抽象类,不能用来定义对象类,不能用
48、来定义对象。虽然抽象类不能定义对象虽然抽象类不能定义对象(或者说抽象类不能实例化或者说抽象类不能实例化),但是但是可以定义指向抽象类数据的指针变量可以定义指向抽象类数据的指针变量。当派生类。当派生类成为具体类之后,就可以用这种指针指向派生类对象,成为具体类之后,就可以用这种指针指向派生类对象,然后通过该指针调用虚函数,实现多态性的操作。然后通过该指针调用虚函数,实现多态性的操作。总结:总结:抽象类的实际作用抽象类的实际作用:定义指向抽象类的指针,实现多:定义指向抽象类的指针,实现多态性操作。即对各纯虚函数可实现多态性操作。态性操作。即对各纯虚函数可实现多态性操作。例例12.4 虚函数和抽象基类
49、的应用。虚函数和抽象基类的应用。在本章例在本章例12.1介绍了以介绍了以Point为基类的为基类的点点圆圆圆柱体圆柱体类的层次结构类的层次结构。现在要对它进行改写,在程序中使用。现在要对它进行改写,在程序中使用虚函数和抽象基类。虚函数和抽象基类。类的层次结构类的层次结构的顶层是的顶层是抽象基类抽象基类Shape(形状形状)。Point(点点),Circle(圆圆),Cylinder(圆柱体圆柱体)都是都是Shape类的直接派生类和间接派生类。类的直接派生类和间接派生类。下面是一个完整的程序,为了便于阅读,分段插入了下面是一个完整的程序,为了便于阅读,分段插入了一些文字说明。一些文字说明。程序如
50、下程序如下:12.4.3 应用实例应用实例第第(1)部分部分#include using namespace std;/声明抽象基类声明抽象基类Shape 因含纯虚函数!因含纯虚函数!class Shape/目的:可使各纯虚函数实现多态性目的:可使各纯虚函数实现多态性public:virtual float area()const return 0.0;/虚函数虚函数 virtual float volume()const return 0.0;/虚函数虚函数 virtual void shapeName()const=0;/纯虚函数纯虚函数;/第第(2)部分部分/声明声明Point类类cla