第6章 多态性与虚函数.docx
- 文档编号:29966037
- 上传时间:2023-08-03
- 格式:DOCX
- 页数:23
- 大小:26.90KB
第6章 多态性与虚函数.docx
《第6章 多态性与虚函数.docx》由会员分享,可在线阅读,更多相关《第6章 多态性与虚函数.docx(23页珍藏版)》请在冰豆网上搜索。
第6章多态性与虚函数
第6章多态性与虚函数
6.1多态性的概念
多态性(polymorphism)是面向对象程序设计的一个重要特征。
利用多态性可以设计和实现一个易于扩展的系统。
有过非面向对象语言开发经历的人,通常对这一章节的内容会觉得不习惯,因为很多人错误地认为,支持类的封装的语言就是支持面向对象的,其实不然,VisualBASIC6.0是典型的非面向对象的开发语言,但是它的确是支持类,支持类并不能说明就是支持面向对象,能够解决多态问题的语言,才是真正支持面向对象的开发的语言,所以务必提醒有过其它非面向对象语言基础的读者注意!
多态的意思是指具有不同功能的函数可以用同一个函数名,这样就可以用一个函数名调用不同内容的函数。
其实,我们已经接触过多态性的现象,例如函数的重载、运算符重载都是多态现象。
只是那时没有用到多态性这一专业术语而已。
例如,使用运算符"+"使两个数值相加,就是发送一个消息,它要调用operator+函数。
实际上,整型、单精度型、双精度型的加法操作过程是互不相同的,是由不同内容的函数实现的。
显然,它们以不同的行为或方法来响应同一消息。
在面向对象方法中一般是这样表述多态性的:
向不同的对象发送同一个消息,不同的对象在接收时会产生不同的行为(即方法)。
也就是说,每个对象可以用自己的方式去响应共同的消息。
从系统实现的角度看,多态性分为两类:
静态多态性和动态多态性。
以前学过的函数重载和运算符重载实现的多态性属于静态多态性,在程序编译时系统就能决定调用的是哪个函数,因此静态多态性又称编译时的多态性。
静态多态性是通过函数的重载实现的(运算符重载实质上也是函数重载)。
动态多态性是在程序运行过程中才动态地确定操作所针对的对象。
它又称运行时的多态性。
动态多态性是通过虚函数(virtualfunction)实现的。
本章中主要介绍动态多态性和虚函数。
要研究的问题是:
当一个基类被继承为不同的派生类时,各派生类可以使用与基类成员相同的成员名,如果在运行时用同一个成员名调用类对象的成员,会调用哪个对象的成员?
也就是说,通过继承而产生了相关的不同的派生类,与基类成员同名的成员在不同的派生类中有不同的含义。
也可以说,多态性是“一个接口,多种方法”。
6.2一个典型的例子
例6.1先建立一个Point(点)类,包含数据成员x,y(坐标点)。
以它为基类,派生出一个Circle(圆)类,增加数据成员r(半径),再以Circle类为直接基类,派生出一个Cylinder(圆柱体)类,再增加数据成员h(高)。
要求编写程序,重载运算符“<<”和“>>”,使之能用于输出以上类对象。
(1)定义基类Point
#include
usingnamespacestd;
//--------------------------Point类的定义
classPoint
{
protected:
doublex,y;
public:
Point(doublea=0,doubleb=0){x=a;y=b;}//构造函数
friendostream&operator<<(ostream&o,constPoint&d)//运算符重载,派生类中有与其同名
{returno<<'('< }; //------------------------------主函数 intmain() { Pointp(3.5,6.4); cout< } (2)定义派生类Circle #include usingnamespacestd; //--------------------------Point类的定义 classPoint{ protected: doublex,y; public: Point(doublea=0,doubleb=0){x=a,y=b;} friendostream&operator<<(ostream&o,constPoint&d)//运算符重载,派生类中有与其同名 {returno<<'('< }; //--------------------------Circle类的定义 classCircle: publicPoint { doubleradius;//增加的数据成员半径 public: Circle(doublea=0,doubleb=0,doubler=0): Point(a,b),radius(r){}//派生类构造函数 doubleArea()const{returnradius*radius*3,1415926;}//增加的成员函数,派生类中也有 friendinlineostream&operator<<(ostream&o,constCircle&c) {returno<<"圆心坐标Center=("< }; //------------------------------主函数 intmain() { Pointp(3.5,6.4); cout< Circlec(3.5,6.4,5.2);//如果不在Circle类中定义<<的重载,就会调用Point中的<<重载 cout< Point&pRef=c;//派生类对象可以替代基类对象为基类对象的引用初始化或赋值。 //pRef不能认为是c的别名,只是c中基类部分的别名 cout< p=c;//派生类对象可以向基类赋值 cout< Pointp1(c);//拷贝 cout< return0; } (3.5,6.4) 圆心坐标Center=(3.5,6.4),半径r=5.2,面积area=1.41593e+006 (3.5,6.4)//是调用基类的operator<< (3.5,6.4)//是调用基类的operator<< (3.5,6.4)//是调用基类的operator<< 如果不在Circle类中定义<<的重载,执行cout< pRef不能认为是c的别名,它只是c中基类部分的别名,与c中基类部分共享同一段存储单元。 所以执行cout< 同样,p1是输出”圆”的信息。 (3)由Circle派生一个圆柱体类Cylinder #include #include usingnamespacestd; //--------------------------Point类的定义 classPoint{ protected: doublex,y; public: Point(doublea=0,doubleb=0){x=a;y=b;} friendinlineostream&operator<<(ostream&o,constPoint&d) {returno<<"Center=("< }; //--------------------------Circle类的定义 classCircle: publicPoint { protected: doubleradius; public: Circle(doublea=0,doubleb=0,doubler=0): Point(a,b),radius(r){} doubleArea()const{returnradius*radius*3,1415926;} friendostream&operator<<(ostream&o,constCircle&c) {returno<<"Center=("< }; //---------------------------Cylinder类的定义 classCylinder: publicCircle { protected: floatheight; public: Cylinder(doublea=0,doubleb=0,doubler=0,doubleh=0): Circle(a,b,r),height(h){} doubleArea()const{return2*Circle: : Area()+2*3,1415926*radius*height;} friendostream&operator<<(ostream&o,constCylinder&cy) {returno<<"圆心坐标Center=("< }; //------------------------------主函数 intmain() { Pointp(3.5,6.4); cout< Circlec(3.5,6.4,5.2);//如果不在Circle类中定义<<,就会调用Point中的<< cout< Cylindercy(3.5,6.4,5.2,10);//如不在Cylinder中定义<<,就会调用Circle类中的<< cout< Point&pRef=cy; cout< Circle&cRef=cy; cout< } 请注意,Cylinder类中定义了Area函数,它与Circle类中的Area函数同名,这两个函数不是重载函数(为什么? )两个同名函数不在同一个类中,而是分别在基类和派生类中,属于同名覆盖,覆盖了父类操作。 注意: 函数operator<<有一个形参不同,所以是重载,而不是覆盖。 在编译时编译系统即可以判定应调用哪个重载运算符函数。 所以是静态多态性。 6.3虚函数 一、虚函数的作用 在类的继承层次结构中,在不同的层次中可以出现名字相同、参数个数和类型都相同而功能不同的函数。 编译系统按照同名覆盖的原则决定调用的对象。 在例6.1程序中用cy.Area()调用的是派生类Cylinder中的成员函数Area。 如果想调用cy中的直接基类Circle的Area函数,应当表示为: cy.Circle: : area()。 用这种方法来区分两个同名的函数。 但是这样做很不方便。 设想能否用同一个调用形式,既能调用派生类又能调用基类的同名函数。 在程序中不是通过不同的对象名去调用不同派生层次中的同名函数,而是通过指针调用它们。 例如,用同一个语句“pt->display();”可以调用不同派生层次中的display函数,只需在调用前给指针变量pt赋以不同的值(使之指向不同的类对象)即可。 C++中的虚函数就是用来解决这个问题的。 虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。 例6.2基类与派生类中有同名函数。 //在下面的程序中Student是基类,Graduate是派生类,它们都有display这个同名的函数。 #include #include usingnamespacestd; //------------------------------------声明基类Student classStudent {public: Student(int,string,float);//声明构造函数 voiddisplay();//声明输出函数 protected: //受保护成员,派生类可以访问 intnum; stringname; floatscore; }; //-------------------------------------------Student类成员函数的实现 Student: : Student(intn,stringnam,floats)//定义构造函数 {num=n;name=nam;score=s;} voidStudent: : display()//定义输出函数 {cout<<"num: "< "< "< //----------------------------------------------------声明公用派生类Graduate classGraduate: publicStudent {public: Graduate(int,string,float,float);//声明构造函数 voiddisplay();//声明输出函数 private: floatpay; }; //----------------------------------------------------------Graduate类成员函数的实现 voidGraduate: : display()//定义输出函数 {cout<<"num: "< "< "< Graduate: : Graduate(intn,stringnam,floats,floatp): Student(n,nam,s),pay(p){} //主函数 intmain() {Studentstud1(1001,"Li",87.5);//定义Student类对象stud1 Graduategrad1(2001,"Wang",98.5,563.5);//定义Graduate类对象grad1 Student*pt=&stud1;//定义指向基类对象的指针变量pt pt->display(); pt=&grad1; pt->display(); return0; } 运行结果: num: 1001 name: Li score: 87.5 num: 2001 name: Wang score: 98.5 从结果可以知道,它们执行的都是基类的display。 下面我们来对程序作一点修改,在Student类中声明display函数时,在最左面加一个关键字virtual,即virtualvoiddisplay();这样就把Student类的display函数声明为虚函数。 程序其他部分都不改动。 运行结果: num: 1001(stud1的数据) name: Li score: 87.5 num: 2001(grad1中基类部分的数据) name: Wang score: 98.5 pay=563.5(这一项以前是没有的) 由虚函数实现的动态多态性就是: 同一类族中不同类的对象,对同一函数调用作出不同的响应。 虚函数的使用方法是: (1)在基类用virtual声明成员函数为虚函数。 这样就可以在派生类中重新定义此函数,为它赋予新的功能,并能方便地被调用。 在类外定义虚函数时,不必再加virtual。 (2)在派生类中重新定义此函数,要求函数名、函数类型、函数参数个数和类型全部与基类的虚函数相同,并根据派生类的需要重新定义函数体。 C++规定,当一个成员函数被声明为虚函数后,其派生类中的同名函数都自动成为虚函数。 因此在派生类重新声明该虚函数时,可以加virtual,也可以不加,但习惯上一般在每一层声明该函数时都加virtual,使程序更加清晰。 如果在派生类中没有对基类的虚函数重新定义,则派生类简单地继承其直接基类的虚函数。 (3)定义一个指向基类对象的指针变量,并使它指向同一类族中需要调用该函数的对象。 (4)通过该指针变量调用此虚函数,此时调用的就是指针变量指向的对象的同名函数。 通过虚函数与指向基类对象的指针变量的配合使用,就能方便地调用同一类族中不同类的同名函数,只要先用基类指针指向即可。 如果指针不断地指向同一类族中不同类的对象,就能不断地调用这些对象中的同名函数。 需要说明;有时在基类中定义的非虚函数会在派生类中被重新定义(如例6.1中的Area函数),如果用基类指针调用该成员函数,则系统会调用对象中基类部分的成员函数;如果用派生类指针调用该成员函数,则系统会调用派生类对象中的成员函数,这并不是多态性行为(使用的是不同类型的指针),没有用到虚函数的功能。 以前介绍的函数重载处理的是同一层次上的同名函数问题,而虚函数处理的是不同派生层次上的同名函数问题,前者是横向重载,后者可以理解为纵向重载。 但与重载不同的是: 同一类族的虚函数的首部是相同的,而函数重载时函数的首部是不同的(参数个数或类型不同)。 二、静态关联与动态关联 编译系统要根据已有的信息,对同名函数的调用作出判断。 对于调用同一类族中的虚函数,应当在调用时用一定的方式告诉编译系统,你要调用的是哪个类对象中的函数。 这样编译系统在对程序进行编译时,即能确定调用的是哪个类对象中的函数。 确定调用的具体对象的过程称为关联(binding)。 在这里是指把一个函数名与一个类对象捆绑在一起,建立关联。 一般地说,关联指把一个标识符和一个存储地址联系起来。 前面所提到的函数重载和通过对象名调用的虚函数,在编译时即可确定其调用的虚函数属于哪一个类,其过程称为静态关联(staticbinding),由于是在运行前进行关联的,故又称为早期关联(earlybinding)。 函数重载属静态关联。 在运行阶段,指针可以先后指向不同的类对象,从而调用同一类族中不同类的虚函数。 由于动态关联是在编译以后的运行阶段进行的,因此也称为滞后关联(latebinding)。 三、在什么情况下应当声明虚函数 使用虚函数时,有两点要注意: (1)只能用virtual声明类的成员函数,使它成为虚函数,而不能将类外的普通函数声明为虚函数。 因为虚函数的作用是允许在派生类中对基类的虚函数重新定义。 显然,它只能用于类的继承层次结构中。 (2)一个成员函数被声明为虚函数后,在同一类族中的类就不能再定义一个非virtual的但与该虚函数具有相同的参数(包括个数和类型)和函数返回值类型的同名函数。 根据什么考虑是否把一个成员函数声明为虚函数呢? 主要考虑以下几点: (1)首先看成员函数所在的类是否会作为基类。 然后看成员函数在类的继承后有无可能被更改功能,如果希望更改其功能的,一般应该将它声明为虚函数。 (2)如果成员函数在类被继承后功能不需修改,或派生类用不到该函数,则不要把它声明为虚函数。 不要仅仅考虑到要作为基类而把类中的所有成员函数都声明为虚函数。 (3)应考虑对成员函数的调用是通过对象名还是通过基类指针或引用去访问,如果是通过基类指针或引用去访问的,则应当声明为虚函数。 (4)有时,在定义虚函数时,并不定义其函数体,即函数体是空的。 它的作用只是定义了一个虚函数名,具体功能留给派生类去添加。 需要说明的是: 使用虚函数,系统要有一定的空间开销。 当一个类带有虚函数时,编译系统会为该类构造一个虚函数表(virtualfunctiontable,简称vtable),它是一个指针数组,存放每个虚函数的入口地址。 系统在进行动态关联时的时间开销是很少的,因此,多态性是高效的。 四、虚析构函数 析构函数的作用是在对象撤销之前做必要的“清理现场”的工作。 当派生类的对象从内存中撤销时一般先调用派生类的析构函数,然后再调用基类的析构函数。 但是,如果用new运算符建立了临时对象,若基类中有析构函数,并且定义了一个指向该基类的指针变量。 在程序用带指针参数的delete运算符撤销对象时,会发生一个情况: 系统会只执行基类的析构函数,而不执行派生类的析构函数。 例6.3基类中有非虚析构函数时的执行情况。 #include usingnamespacestd; classPoint {public: Point(){} ~Point(){cout<<"Point析构函数"< }; classCircle: publicPoint {public: Circle(){} ~Circle(){cout<<"Circle析构函数"< private: intradus; }; intmain() {Point*p=newCircle; deletep; return0; } 从运行结果可以看出,只执行了基类Point的析构函数,而没有执行派生类Circle的析构函数。 原因是以前介绍过的。 如果希望能执行派生类Circle的析构函数,可以将基类的析构函数声明为虚析构函数,如: virtual~Point(){cout<<″Point析构函数″< 加上virtual后,其结果是先调用了派生类的析构函数,再调用基类的析构函数,符合人们的愿望。 如果将基类的析构函数声明为虚函数时,由该基类所派生的所有派生类的析构函数也都自动成为虚函数,即使派生类的析构函数与基类的析构函数名字不相同。 最好把基类的析构函数声明为虚函数。 这将使所有派生类的析构函数自动成为虚函数。 这样,如果程序中显式地用了delete运算符准备删除一个对象,而delete运算符的操作对象用了指向派生类对象的基类指针,则系统会调用相应类的析构函数。 6.4纯虚函数与抽象类 一、纯虚函数 有时在基类中将某一成员函数定为虚函数,并不是基类本身的要求,而是考虑到派生类的需要,在基类中预留了一个函数名,具体功能留给派生类根据需要去定义。 例如在本章的例12.1程序中,基类Point中没有求面积的area函数,因为“点”是没有面积的,也就是说,基类本身不需要这个函数,所以可以在基类Point中加一个area函数,并声明为纯虚函数: virtualfloatarea()=0; 但是,在其直接派生类Circle和间接派生类Cylinder中都需要有area函数,而且这两个area函数的功能不同,一个是求圆面积,一个是求圆柱体表面积。 注意: ①纯虚函数没有函数体;②最后面的“=0”并不表示函数返回值为0,它只起形式上的作用,告诉编译系统“这是纯虚函数”;③这是一个声明语句,最后应有分号。 纯虚函数只有函数的名字而不具备函数的功能,不能被调用。 它只是通知编译系统: “在这里声明一个虚函数,留待派生类中定义”。 在派生类中对此函数提供定义后,它才能具备函数的功能,
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 第6章 多态性与虚函数 多态性 函数