C程序设计第12章多态性与虚函数

上传人:博****1 文档编号:568223684 上传时间:2024-07-23 格式:PPT 页数:56 大小:315KB
返回 下载 相关 举报
C程序设计第12章多态性与虚函数_第1页
第1页 / 共56页
C程序设计第12章多态性与虚函数_第2页
第2页 / 共56页
C程序设计第12章多态性与虚函数_第3页
第3页 / 共56页
C程序设计第12章多态性与虚函数_第4页
第4页 / 共56页
C程序设计第12章多态性与虚函数_第5页
第5页 / 共56页
点击查看更多>>
资源描述

《C程序设计第12章多态性与虚函数》由会员分享,可在线阅读,更多相关《C程序设计第12章多态性与虚函数(56页珍藏版)》请在金锄头文库上搜索。

1、第第12章章 多态性与虚函数多态性与虚函数12.1 多态性的概念多态性的概念12.2 一个典型的例子一个典型的例子12.3 虚函数虚函数12.4 纯虚函数与抽象类纯虚函数与抽象类12.1 多态性的概念多态性的概念l多态性多态性(polymorphism)是面向对象程序设计是面向对象程序设计的一个重要特征。的一个重要特征。l在在C+程序设计中,多态性是指具有不同功能程序设计中,多态性是指具有不同功能的函数可以用同一个函数名,这样就可以用的函数可以用同一个函数名,这样就可以用一个函数名调用不同内容的函数。一个函数名调用不同内容的函数。l在面向对象方法中一般是这样表述多态性的在面向对象方法中一般是这

2、样表述多态性的: 向不同的对象发送同一个消息,不同的对象向不同的对象发送同一个消息,不同的对象在接收时会产生不同的行为。也就是说,每在接收时会产生不同的行为。也就是说,每个对象可以用自己的方式去响应共同的消息。个对象可以用自己的方式去响应共同的消息。l从系统实现的角度看,多态性分为两类从系统实现的角度看,多态性分为两类: 静态多态静态多态性和动态多态性。性和动态多态性。l以前学过的以前学过的函数重载函数重载实现的多态性属于静态多态性,实现的多态性属于静态多态性,在程序编译时系统就能决定调用的是哪个函数,因在程序编译时系统就能决定调用的是哪个函数,因此静态多态性又称编译时的多态性。静态多态性是此

3、静态多态性又称编译时的多态性。静态多态性是通过函数的重载实现的通过函数的重载实现的(运算符重载实质上也是函数运算符重载实质上也是函数重载重载)。l动态多态性是在程序运行过程中才动态地确定操作动态多态性是在程序运行过程中才动态地确定操作所针对的对象。它又称运行时的多态性。动态多态所针对的对象。它又称运行时的多态性。动态多态性是通过虚函数性是通过虚函数(virtual function)实现的。实现的。l有关静态多态性的应用已经介绍过了,在本章中主有关静态多态性的应用已经介绍过了,在本章中主要介绍动态多态性和虚函数。要介绍动态多态性和虚函数。12.2 一个典型的例子一个典型的例子l下面是一个承上启

4、下的例子。下面是一个承上启下的例子。l要研究的问题是要研究的问题是: 当一个基类被继承为不同的当一个基类被继承为不同的派生类时,各派生类可以使用与基类成员相派生类时,各派生类可以使用与基类成员相同的成员名,如果在运行时用同一个成员名同的成员名,如果在运行时用同一个成员名调用类对象的成员,会调用哪个对象的成员调用类对象的成员,会调用哪个对象的成员?l#includelusing namespace std;lclass Studentllprotected:l string name;lpublic:l Student(string n):name(n) l void PrintInfo();l

5、;lvoid Student:PrintInfo()ll coutname: nameendl;llclass Graduate:public Studentllprotected:l string advisor;lpublic:l Graduate(string n,string a):Student(n),advisor(a) l void PrintInfo();l;lvoid Graduate:PrintInfo()ll coutname: name advisor: advisorPrintInfo();llGraduate是是Student的派生类,其实例当然也是一的派生类,其实

6、例当然也是一个个Student,用一个,用一个Student类型的指针来引用理类型的指针来引用理应不成问题,但结果却不是我们所希望的。应不成问题,但结果却不是我们所希望的。问题问题2lvoid fun(Student &s)ll s.PrintInfo();llint main( )ll Graduate b(LI,Zhang);l fun(b);ll在类的继承层次结构中,在不同的层次中可以出现在类的继承层次结构中,在不同的层次中可以出现名字相同、参数个数和类型都相同而功能不同的函名字相同、参数个数和类型都相同而功能不同的函数。编译系统按照数。编译系统按照同名覆盖的原则同名覆盖的原则决定调用的

7、对象。决定调用的对象。lStudent a(Wang);la.PrintInfo(); /调用基类的调用基类的PrintInfo()lGraduate b(LI,Zhang);lb.PrintInfo(); /调用派生类的调用派生类的PrintInfo()lStudent *p=new Graduate(LI,Zhang);lp-PrintInfo(); /调用派生类中被覆盖的继承自基类的调用派生类中被覆盖的继承自基类的PrintInfo()l在问题在问题1的程序中我们是想得到的程序中我们是想得到 如下结果:如下结果:lStudent *p1=new Student(wang);lStuden

8、t *p2=new Graduate(LI,Zhang);lp1-PrintInfo(); /调用基类的调用基类的PrintInfo()lp2-PrintInfo(); /调用派生类的调用派生类的PrintInfo()l即希望用基类指针来引用不同的对象(基类的或派即希望用基类指针来引用不同的对象(基类的或派生类的),并能根据引用的对象不同,调用相应的生类的),并能根据引用的对象不同,调用相应的成员函数(表现出不同的行为)成员函数(表现出不同的行为)“多态多态”。l显然,这里我们未能如愿。显然,这里我们未能如愿。12.3 虚函数虚函数12.3.1 虚函数的作用虚函数的作用lC+中的虚函数就是用来

9、解决上节的问题的。中的虚函数就是用来解决上节的问题的。虚函数的作用是允许在派生类中重新定义与虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。引用来访问基类和派生类中的同名函数。l虚函数的使用方法是:虚函数的使用方法是: l(1) 在基类用在基类用virtual声明成员函数为虚函数。声明成员函数为虚函数。这样就可以在派生类中重新定义此函数,为这样就可以在派生类中重新定义此函数,为它赋予新的功能,并能方便地被调用。它赋予新的功能,并能方便地被调用。l在类外定义虚函数时,不必再加在类外定义虚函数时,

10、不必再加virtual。l(2) 在派生类中重新定义此函数,要求函数名、在派生类中重新定义此函数,要求函数名、函数类型、函数参数个数和类型全部与基类函数类型、函数参数个数和类型全部与基类的虚函数相同,并根据派生类的需要重新定的虚函数相同,并根据派生类的需要重新定义函数体。义函数体。lC+规定,当一个成员函数被声明为虚函数后,其规定,当一个成员函数被声明为虚函数后,其派生类中的同名函数都自动成为虚函数。因此在派派生类中的同名函数都自动成为虚函数。因此在派生类重新声明该虚函数时,可以加生类重新声明该虚函数时,可以加virtual,也可以,也可以不加,但不加,但习惯上习惯上一般在每一层声明该函数时都

11、加一般在每一层声明该函数时都加virtual,使程序更加清晰使程序更加清晰。l如果在派生类中没有对基类的虚函数重新定义,则如果在派生类中没有对基类的虚函数重新定义,则派生类简单地继承其直接基类的虚函数。派生类简单地继承其直接基类的虚函数。l(3) 定义一个指向基类对象的指针变量,并使它指定义一个指向基类对象的指针变量,并使它指向同一类族中需要调用该函数的对象。向同一类族中需要调用该函数的对象。l(4) 通过该指针变量调用此虚函数,此时调用的就通过该指针变量调用此虚函数,此时调用的就是指针变量指向的对象的同名函数。是指针变量指向的对象的同名函数。l通过虚函数与指向基类对象的指针变量的配通过虚函数

12、与指向基类对象的指针变量的配合使用,就能方便地调用同一类族中不同类合使用,就能方便地调用同一类族中不同类的同名函数,只要先用基类指针指向即可。的同名函数,只要先用基类指针指向即可。l#includelusing namespace std;lclass Studentllprotected:l string name;lpublic:l Student(string n):name(n) l virtual void PrintInfo();l;lvoid Student:PrintInfo()ll coutname: nameendl;llclass Graduate:public Stud

13、entllprotected:l string advisor;lpublic:l Graduate(string n,string a):Student(n),advisor(a) l void PrintInfo();l;lvoid Graduate:PrintInfo()ll coutname: name advisor: advisorPrintInfo();l12.3.2 静态关联与动态关联静态关联与动态关联l编译系统要根据已有的信息,对同名函数的调用作编译系统要根据已有的信息,对同名函数的调用作出判断。对于调用同一类族中的虚函数,应当在调出判断。对于调用同一类族中的虚函数,应当在调

14、用时用一定的方式告诉编译系统,你要调用的是哪用时用一定的方式告诉编译系统,你要调用的是哪个类对象中的函数。这样编译系统在对程序进行编个类对象中的函数。这样编译系统在对程序进行编译时,即能确定调用的是哪个类对象中的函数。译时,即能确定调用的是哪个类对象中的函数。l确定调用的具体对象的过程称为确定调用的具体对象的过程称为关联关联(binding)。在。在这里是指把一个函数名与一个类对象捆绑在一起,这里是指把一个函数名与一个类对象捆绑在一起,建立关联。一般地说,关联指把一个标识符和一个建立关联。一般地说,关联指把一个标识符和一个存储地址联系起来。存储地址联系起来。l前面所提到的函数重载和通过对象名调

15、用的虚函数,前面所提到的函数重载和通过对象名调用的虚函数,在编译时即可确定其调用的虚函数属于哪一个类,在编译时即可确定其调用的虚函数属于哪一个类,其过程称为静态关联其过程称为静态关联(static binding),由于是在运,由于是在运行前进行关联的,故又称为早期关联行前进行关联的,故又称为早期关联(early binding)。函数重载属静态关联。函数重载属静态关联。 l在上节程序中看到了怎样使用虚函数,在调用虚函在上节程序中看到了怎样使用虚函数,在调用虚函数时并没有指定对象名,那么系统是怎样确定关联数时并没有指定对象名,那么系统是怎样确定关联的呢?是通过基类指针与虚函数的结合来实现多态的

16、呢?是通过基类指针与虚函数的结合来实现多态性的。先定义了一个指向基类的指针变量,并使它性的。先定义了一个指向基类的指针变量,并使它指向相应的类对象,然后通过这个基类指针去调用指向相应的类对象,然后通过这个基类指针去调用虚函数虚函数(如如“p-PrintInfo()”)。显然,对这样的调用。显然,对这样的调用方式,编译系统在编译该行时是无法确定调用哪一方式,编译系统在编译该行时是无法确定调用哪一个类对象的虚函数的。因为编译只作静态的语法检个类对象的虚函数的。因为编译只作静态的语法检查,光从语句形式是无法确定调用对象的。查,光从语句形式是无法确定调用对象的。l在这样的情况下,编译系统把它放到运行阶

17、段处理,在这样的情况下,编译系统把它放到运行阶段处理,在运行阶段确定关联关系。在运行阶段,基类指针在运行阶段确定关联关系。在运行阶段,基类指针变量先指向了某一个类对象,然后通过此指针变量变量先指向了某一个类对象,然后通过此指针变量调用该对象中的函数。此时调用哪一个对象的函数调用该对象中的函数。此时调用哪一个对象的函数无疑是确定的。由于是在运行阶段把虚函数和类对无疑是确定的。由于是在运行阶段把虚函数和类对象象“绑定绑定”在一起的,因此,此过程称为动态关联在一起的,因此,此过程称为动态关联(dynamic binding)。这种多态性是动态的多态性,。这种多态性是动态的多态性,即运行阶段的多态性。

18、即运行阶段的多态性。l在运行阶段,指针可以先后指向不同的类对象,从在运行阶段,指针可以先后指向不同的类对象,从而调用同一类族中不同类的虚函数。由于动态关联而调用同一类族中不同类的虚函数。由于动态关联是在编译以后的运行阶段进行的,因此也称为滞后是在编译以后的运行阶段进行的,因此也称为滞后关联关联(late binding)。12.3.3 在什么情况下应当声明虚函数在什么情况下应当声明虚函数l使用虚函数时,有两点要注意使用虚函数时,有两点要注意: l(1)只能用只能用virtual声明类的声明类的成员函数成员函数,使它成为虚函,使它成为虚函数,而不能将类外的普通函数声明为虚函数。因为数,而不能将类

19、外的普通函数声明为虚函数。因为虚函数的作用是允许在派生类中对基类的虚函数重虚函数的作用是允许在派生类中对基类的虚函数重新定义。显然,它只能用于类的继承层次结构中。新定义。显然,它只能用于类的继承层次结构中。l(2) 一个成员函数被声明为虚函数后,在同一类族一个成员函数被声明为虚函数后,在同一类族中的类就不能再定义一个非中的类就不能再定义一个非virtual的但与该虚函数的但与该虚函数具有相同的参数具有相同的参数(包括个数和类型包括个数和类型)和函数返回值类和函数返回值类型的同名函数。型的同名函数。l根据什么考虑是否把一个成员函数声明为虚函数呢根据什么考虑是否把一个成员函数声明为虚函数呢?主要考

20、虑以下几点?主要考虑以下几点: l(1) 首先看成员函数所在的类是否会作为基类。然首先看成员函数所在的类是否会作为基类。然后看成员函数在类的继承后有无可能被更改功能,后看成员函数在类的继承后有无可能被更改功能,如果希望更改其功能的,一般应该将它声明为虚函如果希望更改其功能的,一般应该将它声明为虚函数数。l(2) 如果成员函数在类被继承后功能不需修改,或如果成员函数在类被继承后功能不需修改,或派生类用不到该函数,则不要把它声明为虚函数。派生类用不到该函数,则不要把它声明为虚函数。不要仅仅考虑到要作为基类而把类中的所有成员函不要仅仅考虑到要作为基类而把类中的所有成员函数都声明为虚函数。数都声明为虚

21、函数。l(3) 应考虑对成员函数的调用是通过对象名还是通应考虑对成员函数的调用是通过对象名还是通过过基类指针或引用基类指针或引用去访问,如果是通过基类指针或去访问,如果是通过基类指针或引用去访问的,则应当声明为虚函数。引用去访问的,则应当声明为虚函数。l(4) 有时,在定义虚函数时,并不定义其函数体,有时,在定义虚函数时,并不定义其函数体,即函数体是空的。它的作用只是定义了一个虚函数即函数体是空的。它的作用只是定义了一个虚函数名,具体功能留给派生类去添加。在名,具体功能留给派生类去添加。在12.4节中将详节中将详细讨论此问题。细讨论此问题。l需要说明的是需要说明的是: 使用虚函数,系统要有一定

22、的空间使用虚函数,系统要有一定的空间开销。当一个类带有虚函数时,编译系统会为该类开销。当一个类带有虚函数时,编译系统会为该类构造一个虚函数表构造一个虚函数表(virtual function table,简称,简称vtable),它是一个指针数组,存放每个虚函数的入,它是一个指针数组,存放每个虚函数的入口地址。系统在进行动态关联时的时间开销是很少口地址。系统在进行动态关联时的时间开销是很少的,因此,多态性是高效的。的,因此,多态性是高效的。12.3.4 虚析构函数虚析构函数l析构函数的作用是在对象撤销之前做必要的析构函数的作用是在对象撤销之前做必要的“清理现场清理现场”的工作。当派生类的对象从

23、内的工作。当派生类的对象从内存中撤销时一般先调用派生类的析构函数,存中撤销时一般先调用派生类的析构函数,然后再调用基类的析构函数。但是,如果用然后再调用基类的析构函数。但是,如果用new运算符建立了临时对象,若基类中有析运算符建立了临时对象,若基类中有析构函数,并且定义了一个指向该基类的指针构函数,并且定义了一个指向该基类的指针变量。在程序用带指针参数的变量。在程序用带指针参数的delete运算符撤运算符撤销对象时,会发生一个情况销对象时,会发生一个情况: 系统会只执行基系统会只执行基类的析构函数,而不执行派生类的析构函数。类的析构函数,而不执行派生类的析构函数。l#include lusin

24、g namespace std;lclass Point /定义基类定义基类Point类类llpublic:l Point( ) /Point类构造函数类构造函数l Point() /Point类析构函数类析构函数l l coutexecuting Point destructorendl;l l;lclass Circle:public Point /定义派生类定义派生类Circle类类llpublic:l Circle( ) /Circle类构造函数类构造函数l Circle( ) /Circle类析构函数类析构函数l l coutexecuting Circle destructoren

25、dl;l lprivate:l int radius;l;lint main( )ll Point *p=new Circle; /用用new开辟动态存储空间开辟动态存储空间l delete p; /用用delete释放动态存储空间释放动态存储空间l return 0;llp是指向基类的指针变量,指向是指向基类的指针变量,指向new开辟的动开辟的动态存储空间,希望用态存储空间,希望用detele释放释放p所指向的空所指向的空间。但运行结果为间。但运行结果为lexecuting Point destructorl表示只执行了基类表示只执行了基类Point的析构函数,而没有的析构函数,而没有执行派

26、生类执行派生类Circle的析构函数。原因是以前介的析构函数。原因是以前介绍过的。绍过的。l如果希望能执行派生类如果希望能执行派生类Circle的析构函数,可以将基类的析的析构函数,可以将基类的析构函数声明为虚析构函数,如构函数声明为虚析构函数,如lvirtual Point()ll coutexecuting Point destructorendl;ll程序其他部分不改动,再运行程序,结果为程序其他部分不改动,再运行程序,结果为lexecuting Circle destructorlexecuting Point destructorl先调用了派生类的析构函数,再调用了基类的析构函数,符

27、先调用了派生类的析构函数,再调用了基类的析构函数,符合人们的愿望。当基类的析构函数为虚函数时,无论指针指合人们的愿望。当基类的析构函数为虚函数时,无论指针指的是同一类族中的哪一个类对象,系统会采用动态关联,调的是同一类族中的哪一个类对象,系统会采用动态关联,调用相应的析构函数,对该对象进行清理工作。用相应的析构函数,对该对象进行清理工作。l如果将基类的析构函数声明为虚函数时,由该基类如果将基类的析构函数声明为虚函数时,由该基类所派生的所有派生类的析构函数也都所派生的所有派生类的析构函数也都自动成为虚函自动成为虚函数数,即使派生类的析构函数与基类的析构函数名字,即使派生类的析构函数与基类的析构函

28、数名字不相同。不相同。l最好把基类的析构函数声明为虚函数最好把基类的析构函数声明为虚函数。这将使所有。这将使所有派生类的析构函数自动成为虚函数。这样,如果程派生类的析构函数自动成为虚函数。这样,如果程序中显式地用了序中显式地用了delete运算符准备删除一个对象,运算符准备删除一个对象,而而delete运算符的操作对象用了指向派生类对象的运算符的操作对象用了指向派生类对象的基类指针,则系统会调用相应类的析构函数。基类指针,则系统会调用相应类的析构函数。l虚析构函数的概念和用法很简单,但它在面虚析构函数的概念和用法很简单,但它在面向对象程序设计中却是很重要的技巧。向对象程序设计中却是很重要的技巧

29、。专业专业人员一般都习惯声明虚析构函数人员一般都习惯声明虚析构函数,即使基类,即使基类并不需要析构函数,也显式地定义一个函数并不需要析构函数,也显式地定义一个函数体为空的虚析构函数,以保证在撤销动态分体为空的虚析构函数,以保证在撤销动态分配空间时能得到正确的处理。配空间时能得到正确的处理。l构造函数不能声明为虚函数构造函数不能声明为虚函数。这是因为在执。这是因为在执行构造函数时类对象还未完成建立过程,当行构造函数时类对象还未完成建立过程,当然谈不上函数与类对象的绑定。然谈不上函数与类对象的绑定。12.4 纯虚函数与抽象类纯虚函数与抽象类12.4.1 纯虚函数纯虚函数l有时在基类中将某一成员函数

30、定为虚函数,有时在基类中将某一成员函数定为虚函数,并不是基类本身的要求,而是考虑到派生类并不是基类本身的要求,而是考虑到派生类的需要,在基类中预留了一个函数名,具体的需要,在基类中预留了一个函数名,具体功能留给派生类根据需要去定义。功能留给派生类根据需要去定义。l例如在本章的例例如在本章的例12.1程序中,基类程序中,基类Point中没中没有求面积的有求面积的area函数,因为函数,因为“点点”是没有面是没有面积的,也就是说,基类本身不需要这个函数,积的,也就是说,基类本身不需要这个函数,所以在例所以在例12.1程序中的程序中的Point类中没有定义类中没有定义area函数。但是,在其直接派生

31、类函数。但是,在其直接派生类Circle和间和间接派生类接派生类Cylinder中都需要有中都需要有area函数,而函数,而且这两个且这两个area函数的功能不同,一个是求圆函数的功能不同,一个是求圆面积,一个是求圆柱体表面积。面积,一个是求圆柱体表面积。l在这种情况下应当将在这种情况下应当将area声明为虚函数。可声明为虚函数。可以在基类以在基类Point中加一个中加一个area函数,并声明为函数,并声明为虚函数虚函数: lvirtual float area( ) const return 0;l其返回值为其返回值为0,表示,表示“点点”是没有面积的。其是没有面积的。其实,在基类中并不使用

32、这个函数,其返回值实,在基类中并不使用这个函数,其返回值也是没有意义的。也是没有意义的。l为简化,可以不写出这种无意义的函数体,为简化,可以不写出这种无意义的函数体,只给出函数的原型,并在后面加上只给出函数的原型,并在后面加上“=0”,如如lvirtual float area( ) const =0;/纯虚函数纯虚函数l这就将这就将area声明为一个纯虚函数声明为一个纯虚函数(pure virtual function)。纯虚函数纯虚函数是在声明虚函数时被是在声明虚函数时被“初始化初始化”为为0的函数。的函数。l声明纯虚函数的一般形式是:声明纯虚函数的一般形式是:lvirtual 函数类型函

33、数类型 函数名函数名 (参数表列参数表列) =0;注意:注意:l纯虚函数没有函数体;纯虚函数没有函数体;l最后面的最后面的“=0”并不表示函数返回值为并不表示函数返回值为0,它只起它只起形式上形式上的作用,告诉编译系统的作用,告诉编译系统“这是这是纯虚函数纯虚函数”; l这是一个声明语句,最后应有分号。这是一个声明语句,最后应有分号。l纯虚函数只有函数的名字而不具备函数的功能,不纯虚函数只有函数的名字而不具备函数的功能,不能被调用。它只是通知编译系统能被调用。它只是通知编译系统: “在这里声明一在这里声明一个虚函数,留待派生类中定义个虚函数,留待派生类中定义”。在派生类中对此。在派生类中对此函

34、数提供定义后,它才能具备函数的功能,可被调函数提供定义后,它才能具备函数的功能,可被调用。用。l纯虚函数的作用是在基类中为其派生类保留一个函纯虚函数的作用是在基类中为其派生类保留一个函数的名字,以便派生类根据需要对它进行定义。如数的名字,以便派生类根据需要对它进行定义。如果在基类中没有保留函数名字,则无法实现多态性。果在基类中没有保留函数名字,则无法实现多态性。l如果在一个类中声明了纯虚函数,而在其派生类中如果在一个类中声明了纯虚函数,而在其派生类中没有对该函数定义,则该虚函数在派生类中没有对该函数定义,则该虚函数在派生类中仍然仍然为为纯虚函数。纯虚函数。12.4.2 抽象类抽象类l如果声明了

35、一个类,一般可以用它定义对象。如果声明了一个类,一般可以用它定义对象。但是在面向对象程序设计中,往往有一些类,但是在面向对象程序设计中,往往有一些类,它们不用来生成对象。定义这些类的惟一目它们不用来生成对象。定义这些类的惟一目的是用它的是用它作为基类去建立派生类作为基类去建立派生类。它们作为。它们作为一种基本类型提供给用户,用户在这个基础一种基本类型提供给用户,用户在这个基础上根据自己的需要定义出功能各异的派生类。上根据自己的需要定义出功能各异的派生类。用这些派生类去建立对象。用这些派生类去建立对象。l一个优秀的软件工作者在开发一个大的软件一个优秀的软件工作者在开发一个大的软件时,决不会从头到

36、尾都由自己编写程序代码,时,决不会从头到尾都由自己编写程序代码,他会充分利用已有资源他会充分利用已有资源(例如类库例如类库)作为自己作为自己工作的基础。工作的基础。l这种不用来定义对象而只作为一种基本类型这种不用来定义对象而只作为一种基本类型用作继承的类,称为抽象类用作继承的类,称为抽象类(abstract class),由于它常用作基类,通常称为抽象基类,由于它常用作基类,通常称为抽象基类(abstract base class)。l凡是包含纯虚函数的类都是抽象类凡是包含纯虚函数的类都是抽象类。因为纯虚函数。因为纯虚函数是不能被调用的,包含纯虚函数的类是无法建立对是不能被调用的,包含纯虚函数

37、的类是无法建立对象的。抽象类的作用是作为一个类族的共同基类,象的。抽象类的作用是作为一个类族的共同基类,或者说,为一个类族提供一个或者说,为一个类族提供一个公共接口公共接口。l一个类层次结构中当然也可不包含任何抽象类,每一个类层次结构中当然也可不包含任何抽象类,每一层次的类都是实际可用的,可以用来建立对象的。一层次的类都是实际可用的,可以用来建立对象的。但是,许多好的面向对象的系统,其层次结构的顶但是,许多好的面向对象的系统,其层次结构的顶部是一个抽象类,甚至顶部有好几层都是抽象类。部是一个抽象类,甚至顶部有好几层都是抽象类。l如果在抽象类所派生出的新类中对基类的所有纯虚如果在抽象类所派生出的

38、新类中对基类的所有纯虚函数进行了定义,那么这些函数就被赋予了功能,函数进行了定义,那么这些函数就被赋予了功能,可以被调用。这个派生类就不是抽象类,而是可以可以被调用。这个派生类就不是抽象类,而是可以用来定义对象的具体类用来定义对象的具体类(concrete class)。如果在派。如果在派生类中没有对所有纯虚函数进行定义,则此派生类生类中没有对所有纯虚函数进行定义,则此派生类仍然是抽象类,不能用来定义对象。仍然是抽象类,不能用来定义对象。l虽然抽象类不能定义对象虽然抽象类不能定义对象(或者说抽象类不能实例化或者说抽象类不能实例化),但是但是可以定义指向抽象类数据的指针变量。当派可以定义指向抽象

39、类数据的指针变量。当派生类成为具体类之后,就可以用这种指针指向派生生类成为具体类之后,就可以用这种指针指向派生类对象,然后通过该指针调用虚函数,实现多态性类对象,然后通过该指针调用虚函数,实现多态性的操作。的操作。12.4.3 应用实例应用实例l虚函数和抽象基类的应用。虚函数和抽象基类的应用。l在本章例在本章例12.1介绍了以介绍了以Point为基类的点为基类的点圆圆圆柱圆柱体类的层次结构。现在要对它进行改写,在程序中体类的层次结构。现在要对它进行改写,在程序中使用虚函数和抽象基类。类的层次结构的顶层是抽使用虚函数和抽象基类。类的层次结构的顶层是抽象基类象基类Shape(形状形状)。Point

40、(点点), Circle(圆圆), Cylinder(圆柱体圆柱体)都是都是Shape类的直接派生类和间类的直接派生类和间接派生类。接派生类。l下面是一个完整的程序,为了便于阅读,分段插入下面是一个完整的程序,为了便于阅读,分段插入了一些文字说明。了一些文字说明。l程序如下:程序如下:l#include lusing namespace std;l/声明抽象基类声明抽象基类Shapelclass Shapellpublic:l virtual double Area( )l l return 0.0; /虚函数虚函数l l virtual double Volume()l l return 0

41、.0; /虚函数虚函数l l virtual void ShapeName() =0; /纯虚函数纯虚函数l;l/声明声明Point类类lclass Point:public Shape/Point是是Shape的公用派生类的公用派生类llprotected:l double x,y;lpublic:l Point(double=0,double=0);l void SetPoint(double,double);l double GetX( ) return x; l double GetY( ) return y; l virtual void ShapeName( ) l coutPoi

42、nt: ; /对虚函数进行再定义对虚函数进行再定义l l void Print();l;l/定义定义Point类成员函数类成员函数lPoint:Point(double a,double b)ll x=a;l y=b;llvoid Point:SetPoint(double a,double b)ll x=a;l y=b;llvoid Point:Print()ll coutx,y;ll/声明声明Circle类类lclass Circle:public Pointllprotected:l double radius;lpublic:l Circle(double x=0,double y=0

43、,double r=0);l void SetRadius(double);l double GetRadius( );l virtual double Area( );l virtual void ShapeName( )l l coutCircle:; /对虚函数进行再定义对虚函数进行再定义l l void Print();l;l/实现实现Circle类成员函数类成员函数lCircle:Circle(double a,double b,double r):Point(a,b),radius(r) lvoid Circle:SetRadius(double r)l radius=r;lldo

44、uble Circle:GetRadius( )l return radius;lldouble Circle:Area( )l return 3.14159*radius*radius;llvoid Circle:Print()l Point:Print();l cout,r=radius;ll/声明声明Cylinder类类lclass Cylinder:public Circlellprotected:l double height;lpublic:l Cylinder (double x=0,double y=0,double r=0,double h=0);l void SetHeig

45、ht(double);l virtual double Area( );l virtual double Volume( );l virtual void ShapeName( )l l coutCylinder:; /对虚函数进行再定义对虚函数进行再定义l l void Print();l;l/定义定义Cylinder类成员函数类成员函数lCylinder:Cylinder(double a,double b,double r,double h)l :Circle(a,b,r),height(h) lvoid Cylinder:SetHeight(double h) l height=h;l

46、ldouble Cylinder:Area( )l return 2*Circle:area( )+2*3.14159*radius*height;lldouble Cylinder:Volume( )l return Circle:area( )*height;llvoid Cylinder:Print()l Circle:Print();l cout ,h=height;ll/main函数函数lint main( )ll Point point(3.2,4.5);/建立建立Point类对象类对象pointl Circle circle(2.4,1.2,5.6); /建立建立Circle类对

47、象类对象circlel Cylinder cylinder(3.5,6.4,5.2,10.5); /建立建立Cylinder类对象类对象cylinderl point.ShapeName(); /静态关联静态关联l point.Print();l coutendl;l circle.ShapeName(); /静态关联静态关联l circle.Print();l coutendl;l cylinder.ShapeName(); /静态关联静态关联l cylinder.Print();l coutendlShapeName( ); /动态关联动态关联 coutx=point.GetX( ),y=

48、point.GetY( )narea=Area( )nvolume=Volume()ShapeName( ); /动态关联动态关联 coutx=circle.GetX( ),y=circle.GetY( )narea=Area( ) nvolume=volume( )ShapeName( ); /动态关联动态关联 coutx=cylinder.GetX( ),y=cylinder.GetY( )narea=Area( )nvolume=Volume( )nn; return 0;l从本例可以进一步明确以下结论从本例可以进一步明确以下结论:l(1) 一个基类如果包含一个或一个以上纯虚函数,一个基

49、类如果包含一个或一个以上纯虚函数,就是抽象基类。抽象基类不能也不必要定义对象。就是抽象基类。抽象基类不能也不必要定义对象。l(2) 抽象基类与普通基类不同,它一般并不是现实抽象基类与普通基类不同,它一般并不是现实存在的对象的抽象存在的对象的抽象(例如圆形例如圆形(Circle)就是千千万万就是千千万万个实际的圆的抽象个实际的圆的抽象),它可以没有任何物理上的或其,它可以没有任何物理上的或其他实际意义方面的含义。他实际意义方面的含义。l(3) 在类的层次结构中,顶层或最上面的几层可以在类的层次结构中,顶层或最上面的几层可以是抽象基类。抽象基类体现了是抽象基类。抽象基类体现了本类族中各类的共性本类

50、族中各类的共性,把各类中共有的成员函数集中在抽象基类中声明。把各类中共有的成员函数集中在抽象基类中声明。l(4) 抽象基类是本类族的抽象基类是本类族的公共接口公共接口。或者说,从同。或者说,从同一基类派生出的多个类有同一接口。一基类派生出的多个类有同一接口。 l(5) 区别静态关联和动态关联。区别静态关联和动态关联。l(6) 如果在基类声明了虚函数,则在派生类中凡是如果在基类声明了虚函数,则在派生类中凡是与该函数有相同的函数名、函数类型、参数个数和与该函数有相同的函数名、函数类型、参数个数和类型的函数,均为虚函数类型的函数,均为虚函数(不论在派生类中是否用不论在派生类中是否用virtual声明

51、声明)。l(7) 使用虚函数提高了程序的可扩充性。使用虚函数提高了程序的可扩充性。l把类的声明与类的使用分离。这对于设计类库的软把类的声明与类的使用分离。这对于设计类库的软件开发商来说尤为重要。开发商设计了各种各样的件开发商来说尤为重要。开发商设计了各种各样的类,但不向用户提供源代码,用户可以不知道类是类,但不向用户提供源代码,用户可以不知道类是怎样声明的,但是可以使用这些类来派生出自己的怎样声明的,但是可以使用这些类来派生出自己的类。类。l利用虚函数和多态性,程序员的注意力集中在处理利用虚函数和多态性,程序员的注意力集中在处理普遍性,而让执行环境处理特殊性。普遍性,而让执行环境处理特殊性。l多态性把操作的细节留给类的设计者多态性把操作的细节留给类的设计者(他们多他们多为专业人员为专业人员)去完成,而让程序人员去完成,而让程序人员(类的使类的使用者用者)只需要做一些宏观性的工作,告诉系统只需要做一些宏观性的工作,告诉系统做什么,而不必考虑怎么做,极大地简化了做什么,而不必考虑怎么做,极大地简化了应用程序的编码工作,大大减轻了程序员的应用程序的编码工作,大大减轻了程序员的负担,也降低了学习和使用负担,也降低了学习和使用C+编程的难度,编程的难度,使更多的人能更快地进入使更多的人能更快地进入C+程序设计的大门。程序设计的大门。

展开阅读全文
相关资源
正为您匹配相似的精品文档
相关搜索

最新文档


当前位置:首页 > 办公文档 > 工作计划

电脑版 |金锄头文库版权所有
经营许可证:蜀ICP备13022795号 | 川公网安备 51140202000112号