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