




版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
第5章多态性与虚函数5.1多态性的概念5.2一个典型的例子5.3虚函数5.4纯虚函数与抽象类5.1多态性的概念在C++中,基类指针或引用可以直接引用其任何派生子类,而无需程序员介入——这种“用基类的指针或引用操纵多个类型”的能力称为多态(polymorphism)。多态性(polymorphism)是面向对象程序设计的一个重要特征。利用多态性可以设计和实现一个易于扩展的系统。例如,已知基类Camera派生出两个子类OrthographicCamera和PerspectiveCamera:已知函数:
voidlookAt(constCamera*pcamera);CameraPerspectiveCameraOrthographicCamera每次lookAt()函数调用时都会传入一个Camera子类对象的地址,编译器会自动地把它转换成适当的基类指针。例如:
OrthographicCamera
ocam;
lookAt(&ocam);//ok:自动转换成Camera*
PerspectiveCamera*pcam=newPerspectiveCamera;
lookAt(pcam);//ok:自动被转换成Camera*lookAt()的实现被屏蔽在应用程序的实际Camera子类之外,如果以后增加或删除一个子类,无需改变lookAt().子类多态性使得我们在编写应用程序的核心时,可以不用考虑将来需要维护的单个类型。从系统实现的角度看,多态性分为两类:静态多态性和动态多态性。静态多态性是通过函数的重载实现的。利用基类指针和引用,对抽象基类的公有接口进行编程。在运行时刻,真正要引用的类型被解析出来,并且调用适当的公有接口实例。多态性是“一个接口,多种方法”。在运行时刻解析出被调用的函数,这个解析过程被称为动态绑定(dynamicbinding)。在C++中,通过虚拟函数机制来支持动态绑定。通过继承和动态绑定,子类型多态性为面向对象的程序设计提供了基础。5.2一个典型的例子下面是一个承上启下的例子。一方面它是有关继承和运算符重载内容的综合应用的例子,通过这个例子可以进一步融会贯通前面所学的内容,另一方面又是作为讨论多态性的一个基础用例。例5.1:
先建立一个Point(点)类,包含数据成员x,y(坐标点)。以它为基类,派生出一个Circle(圆)类,增加数据成员r(半径),再以Circle类为直接基类,派生出一个Cylinder(圆柱体)类,再增加数据成员h(高)。要求编写程序,重载运算符“<<”和“>>”,使之能用于输出以上类对象。声明基类Point类可写出声明基类Point的部分如下:#include<iostream>//声明类PointclassPoint{public:
Point(floatx=0,floaty=0);//有默认参数的构造函数 voidsetPoint(float,float);//设置坐标值
floatgetX()const{returnx;}//读x坐标
floatgetY()const{returny;}//读y坐标
friendostream&operator<<(ostream&,constPoint&);//重载运算符“<<”protected://受保护成员
floatx,y;};//下面定义Point类的成员函数//Point的构造函数Point::Point(floata,floatb)//对x,y初始化{x=a;y=b;}//设置x和y的坐标值voidPoint::setPoint(floata,floatb)//为x,y赋新值{x=a;y=b;}//重载运算符“<<”,使之能输出点的坐标ostream&operator<<(ostream&output,constPoint&p){output<<″[″<<p.x<<″,″<<p.y<<″]″<<endl;returnoutput;}编写测试程序main函数:intmain(){Pointp(3.5,5.4);//建立Point类对象p
cout<<″x=″<<p.getX()<<″,y=″<<p.getY()<<endl;//输出p的坐标值
p.setPoint(8.5,5.8);//重新设置p的坐标值
cout<<″p(new):″<<p<<endl;//用重载运算符“<<”输出p点坐标}程序编译通过,运行结果为x=3.5,y=5.4p(new):[8.5,5.8]测试程序检查了基类中各函数的功能,以及运算符重载的作用,证明程序是正确的。(2)声明派生类CircleclassCircle:publicPoint//circle是Point类的公用派生类{public:
Circle(floatx=0,floaty=0,floatr=0);//构造函数
voidsetRadius(float);//设置半径值
floatgetRadius()const;//读取半径值
floatarea()const;//计算圆面积
friendostream&operator<<(ostream&,constCircle&);//重载运算符“<<”
private: floatradius;};//定义构造函数,对圆心坐标和半径初始化Circle::Circle(float
a,float
b,float
r):Point(a,b),radius(r){}//设置半径值voidCircle::setRadius(floatr){radius=r;}//读取半径值floatCircle::getRadius()const{returnradius;}//计算圆面积floatCircle::area()const{return3.14159*radius*radius;}//重载运算符“<<”,使之按规定的形式输出圆的信息ostream&operator<<(ostream&output,constCircle&c){output<<″Center=[″<<c.x<<″,″<<c.y<<″],r=″<<c.radius<<″,area=″<<c.area()<<endl;returnoutput;}测试Circle类定义的主函数:intmain(){Circlec(3.5,5.4,5.2);//建立Circle类对象c,并给定圆心坐标和半径
cout<<″originalcircle:\\nx=″<<c.getX()<<″,y=″<<c.getY()<<″,r=″<<c.getRadius()<<″,area=″<<c.area()<<endl;//输出圆心坐标、半径和面积
c.setRadius(7.5);//设置半径值
c.setPoint(5,5);//设置圆心坐标值x,y
cout<<″newcircle:\\n″<<c;//用重载运算符“<<”输出圆对象的信息
Point&pRef=c;//pRef是Point类的引用变量,被c初始化
cout<<″pRef:″<<pRef;//输出pRef的信息
return0;}程序编译通过,运行结果为:originalcircle:x=3.5,y=5.4,r=5.2,area=84.9485newcircle:Center=[5,5],r=7.5,area=175.714pRef:[5,5](3)声明Circle的派生类CylinderclassCylinder:publicCircle//Cylinder是Circle的公用派生类{public: Cylinder(floatx=0,floaty=0,floatr=0,floath=0);
voidsetHeight(float);//设置圆柱高
floatgetHeight()const;//读取圆柱高
floatarea()const;//计算圆表面积
floatvolume()const;//计算圆柱体积
friendostream&operator<<(ostream&,constCylinder&);
protected: floatheight;//圆柱高};//定义构造函数Cylinder::Cylinder(float
a,float
b,float
r,floath):Circle(a,b,r),height(h){}//设置圆柱高voidCylinder::setHeight(float
h){height=h;}//读取圆柱高floatCylinder::getHeight()const{returnheight;}//计算圆表面积floatCylinder::area()const{return2*Circle::area()+2*3.14159*radius*height;}//计算圆柱体积floatCylinder::volume()const{returnCircle::area()*height;}//重载运算符“<<”ostream&operator<<(ostream&output,constCylinder&cy){output<<″Center=[″<<cy.x<<″,″<<cy.y<<″],r=″<<cy.radius<<″,h=″<<cy.height<<″\\narea=″<<cy.area()<<″,volume=″<<cy.volume()<<endl;returnoutput;}写出下面的测试主函数:intmain(){ Cylindercy1(3.5,5.4,5.2,10);//定义Cylinder类对象cy1
cout<<″\\noriginalcylinder:\\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
cout<<″\\nnewcylinder:\\n″<<cy1;//用重载运算符“<<”输出cy1的数据
Point&pRef=cy1;//pRef是Point类对象的引用变量
cout<<″\\npRefasaPoint:″<<pRef;//pRef作为一个“点”输出
Circle&cRef=cy1;//cRef是Circle类对象的引用变量
cout<<″\\ncRefasaCircle:″<<cRef;//cRef作为一个“圆”输出
return0;}运行结果如下:originalcylinder:(输出cy1的初始值)x=3.5,y=5.4,r=5.2,h=10(圆心坐标x,y。半径r,高h)area=495.523,volume=849.485(圆柱表面积area和体积volume)newcylinder:(输出cy1的新值)Center=[5,5],r=7.5,h=15(以[5,5]形式输出圆心坐标)area=1050.29,volume=2550.72(圆柱表面积area和体积volume)pRefasaPoint:[5,5](pRef作为一个“点”输出)cRefasaCircle:Center=[5,5],r=7.5,area=175.714(cRef作为一个“圆”输出)在本例中存在静态多态性,这是运算符重载引起的。可以看到,在编译时编译系统即可以判定应调用哪个重载运算符函数。5.3虚函数
5.3.1虚函数的作用在类的继承层次结构中,在不同的层次中可以出现名字相同、参数个数和类型都相同而功能不同的函数。编译系统按照同名覆盖的原则决定调用的对象。例如:例5.1程序中cy1.area()调用的是派生类Cylinder中的成员函数area。如果想调用cy1中的直接基类Circle的area函数,改如何调用呢?应当表示为:cy1.Circle::area()。用这种方法来区分两个同名的函数很不方便。能否用同一个调用形式,既能调用派生类又能调用基类的同名函数?可以在程序中通过指针调用基类和派生类的同名函数,根据指针实际指向的对象来调用对应的函数。例如,语句pt->display();
可以调用不同派生层次中的display函数,只需在调用前给指针变量pt赋以不同的值(使之指向不同的类对象)即可。C++通过虚函数来解决这个问题。虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。我们来分析例5.2。这个例子开始时没有使用虚函数,然后再讨论使用虚函数的情况。例5.2基类Student与派生类Graduate有同名函数。#include<iostream>#include<string>usingnamespacestd;//声明基类StudentclassStudent{ 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(int,string,float,float);//声明构造函数
voiddisplay();//声明输出函数private:floatpay;};//Graduate类成员函数的实现Graduate::Graduate(intn,stringnam,float
s,float
p):Student(n,nam,s),pay(p){}voidGraduate::display()//定义输出函数{cout<<″num:″<<num<<″\\nname:″<<name<<″\\nscore:″<<score<<″\\npay=″<<pay<<endl;}//主函数intmain(){ Studentstud1(1001,“Li”,87.5); Graduategrad1(2001,“Wang”,98.5,563.5); Student*pt=&stud1; pt->display(); pt=&grad1; pt->display(); return0;}运行结果如下,请仔细分析。 num:1001
name:Li score:87.5 num:2001
name:wang score:98.5对程序作一点修改,在Student类中声明display函数时,在最左面加一个关键字virtual,即
virtualvoiddisplay();再编译和运行程序,请注意分析运行结果:num:1001name:Liscore:87.5num:2001name:wangscore:98.5pay=12005.3.2C++中的虚函数假定我们要为汽车零件商店设计一个记帐程序。希望该程序非常灵活,能够兼容以后出现的新销售情况。为了适应这种情况,将计算账单的函数设为虚函数。设计基类Sale,Sale类对应于单件商品的简单销售。所有类型的销售(如打折销售)都是Sale类的派生类。由虚函数实现的动态多态性就是:基类的指针或引用可以指向其任意派生类的能力。虚函数的使用方法是:在基类用virtual声明成员函数为虚函数。在派生类中重新定义此函数,要求函数名、函数类型、函数参数个数和类型全部与基类的虚函数相同,并根据派生类的需要重新定义函数体。定义一个指向基类对象的指针变量,并使它指向同一类继承层次结构中的某个对象。通过该指针变量调用此虚函数,此时调用的就是指针变量指向的对象的同名函数。C++规定,当一个成员函数被声明为虚函数后,其派生类中的同名函数都自动成为虚函数。在派生类重新声明该虚函数时,可以加virtual,也可以不加,但习惯上一般在每一层声明该函数时都加virtual,使程序更加清晰。如果在派生类中没有对基类的虚函数重新定义,则派生类简单地继承其直接基类的虚函数。特别提醒:有时在基类中定义的非虚函数会在派生类中被重新定义(如例5.1中的area函数),此时不存在多态性。5.3.3静态关联与动态关联计算机系统如何选择正确的调用对象,从而实现多态性?确定调用的具体对象的过程称为关联(binding)。在这里是指把一个函数名与一个类对象捆绑在一起,建立关联。一般地说,关联指把一个标识符和一个存储地址联系起来。静态关联(staticbinding)和动态关联(dynamicbinding)(晚期绑定)在编译时即可确定其调用的函数属于哪一个类,其过程称为静态关联(staticbinding),又称为早期关联(earlybinding)。函数重载属静态关联。对于通过基类指针调用虚函数,编译系统在编译该行时是无法确定调用哪一个类对象的虚函数的。在这样的情况下,在运行阶段确定关联关系。此过程称为动态关联(dynamicbinding)。这种多态性是运行阶段的多态性。在运行阶段,指针可以先后指向不同的类对象,从而调用同一类族中不同类的虚函数。由于动态关联是在编译以后的运行阶段进行的,因此也称为晚期关联(latebinding)。5.3.4在什么情况下应当声明虚函数使用虚函数时,有两点要注意:(1)虚函数只能作用于类的继承层次结构中,即多态性只存在于类继承层次中。(2)一个成员函数被声明为虚函数后,在同一类族中的类就不能再定义一个非virtual的但与该虚函数具有相同的参数(包括个数和类型)和函数返回值类型的同名函数。根据什么考虑是否把一个成员函数声明为虚函数呢?主要考虑以下几点:(1)首先看成员函数所在的类是否会作为基类。然后看成员函数在类的继承后有无可能被更改功能,如果希望更改其功能的,一般应该将它声明为虚函数。(2)如果成员函数在类被继承后功能不需修改,或派生类用不到该函数,则不要把它声明为虚函数。(3)应考虑对成员函数的调用是通过对象名还是通过基类指针或引用去访问,如果是通过基类指针或引用去访问的,则应当声明为虚函数。(4)有时会在基类定义具有空函数体的虚函数。它的作用只是定义了一个虚函数名(即公有接口),然后通过派生类来添加具体功能。在5.4节中将详细讨论此问题。(5)编译器和运行环境需要为虚函数做较多的工作,所以,如果将不必要设置为虚函数的函数标记为virtual,会影响程序的执行效率。5.3.5虚析构函数继承机制下的析构函数的行为如下:派生类的析构函数先被调用。完成之后,直接基类的析构函数被静态调用。但是,如果用new运算符建立了临时对象,若基类中有析构函数,并且定义了一个指向该基类的指针变量。在程序用带指针参数的delete运算符撤销对象时,会发生一个情况:系统会只执行基类的析构函数,而不执行派生类的析构函数。例5.3基类中有非虚析构函数时的执行情况。(为简化程序,只列出最必要的部分。)#include<iostream>usingnamespacestd;classPoint//定义基类Point类{public:Point(){}//Point类构造函数~Point(){cout<<″executingPointdestructor″<<endl;}//Point类析构函数};classCircle:publicPoint//定义派生类Circle类{public:Circle(){}//Circle类构造函数~Circle(){cout<<″executingCircledestructor″<<endl;}//Circle类析构函数
private:
intradius;};intmain(){Point*p=newCircle;//用new开辟动态存储空间deletep;//用delete释放动态存储空间return0;}运行结果为:executingPointdestructor表示只执行了基类Point的析构函数,而没有执行派生类Circle的析构函数。为了执行派生类Circle的析构函数,需要将基类的析构函数声明为虚析构函数,即
virtual~Point(){cout<<″executingPointdestructor″<<endl;}程序其他部分不改动,再运行程序,结果为 executingCircledestructor executingPointdestructor如果将基类的析构函数声明为虚函数时,由该基类所派生的所有派生类的析构函数也都自动成为虚函数,即使派生类的析构函数与基类的析构函数名字不相同。最好把基类的析构函数声明为虚函数。这将使所有派生类的析构函数自动成为虚函数。构造函数不能声明为虚函数。这是因为在执行构造函数时类对象还未完成建立过程,当然谈不上函数与类对象的绑定。5.4纯虚函数与抽象类
5.4.1纯虚函数C++语言提供了一种语法结构,通过它表明,一个虚拟函数只是提供了一个可被派生子类型改写的接口,它本身并不能通过虚拟机制被调用——纯虚拟函数例如在例5.1程序中,基类Point可以提供求面积的纯虚拟函数:
virtualfloatarea()const=0;//纯虚函数其直接派生类Circle和间接派生类Cylinder可以改写这个area函数,分别为求圆面积和求圆柱体表面积。纯虚函数是在声明虚函数时被“初始化”为0的函数。声明纯虚函数的一般形式是 virtual函数类型函数名(参数表列)=0; 注意: ①纯虚函数没有函数体; ②最后面的“=0”并不表示函数返回值为0,它只起形式上的作用,告诉编译系统“这是纯虚函数”; ③这是一个声明语句,最后应有分号。5.4.2抽象类包含一个或多个纯虚函数的类称为抽象基类(abstractclass)。试图创建一个抽象基类的独立类对象会导致编译错误。抽象类的作用是作为一个类继承层次结构的共同基类,或者说,为一个类继承层次结构提供一个公共接口。这种类不用来定义对象,一般只作为一种被继承的基本类型。如果在派生类中对基类的所有纯虚函数进行了定义,那么这些函数就被赋予了功能,可以被调用。这时派生类是可以用来定义对象的具体类(concreteclass);否则派生类仍然是抽象类,不能用来定义对象。我们可以定义指向抽象类对象的指针变量,用此指针指向具体派生类的对象,然后通过该指针调用虚函数,实现多态性的操作。5.4.3应用实例例5.4虚函数和抽象基类的应用。在例5.1介绍了以Point为基类的点—圆—圆柱体类的层次结构。现在对它进行改写,在程序中使用虚函数和抽象基类。类的层次结构的顶层是抽象基类Shape(形状)。Point(点),Circle(圆),Cylinder(圆柱体)都是Shape类的直接派生类和间接派生类。程序如下:第(1)部分#include<iostream>usingnamespacestd;//声明抽象基类ShapeclassShape{ public:
virtualfloatarea()const{return0.0;}
virtualfloatvolume()const{return0.0;}
virtualvoidshapeName()const=0;
};
第(2)部分//声明Point类classPoint:publicShape//Point是Shape的公用派生类{public:
Point(float=0,float=0); voidsetPoint(float,float); floatgetX()const{returnx;} floatgetY()const{returny;} virtualvoidshapeName()const{cout<<″Point:″;} //对虚函数进行再定义
friendostream&operator<<(ostream&,constPoint&); protected: floatx,y;};//定义Point类成员函数Point::Point(float
a,floatb){x=a;y=b;}voidPoint::setPoint(float
a,floatb){x=a;y=b;}ostream&operator<<(ostream&output,constPoint&p){ output<<″[″<<p.x<<″,″<<p.y<<″]″; returnoutput;}第(3)部分//声明Circle类classCircle:publicPoint{ public:
Circle(floatx=0,floaty=0,floatr=0); voidsetRadius(float); floatgetRadius()const; virtualfloatarea()const; virtualvoidshapeName()const {cout<<″Circle:″;}//对虚函数进行再定义
friendostream&operator<<(ostream&,constCircle&); protected: floatradius;};//声明Circle类成员函数Circle::Circle(float
a,float
b,float
r):Point(a,b),radius(r){}voidCircle::setRadius(float
r):radius(r){}floatCircle::getRadius()const{returnradius;}floatCircle::area()const{return3.14159*radius*radius;}ostream&operator<<(ostream&output,constCircle&c){ output<<″[″<<c.x<<″,″<<c.y<<″],r=″<<c.radius; returnoutput;}第(4)部分//声明Cylinder类classCylinder:publicCircle{ public: Cylinder(floatx=0,floaty=0,floatr=0,floath=0); voidsetHeight(float); virtualfloatarea()const; virtualfloatvolume()const; virtualvoidshapeName()const {cout<<″Cylinder:″;}//对虚函数进行再定义
friendostream&operator<<(ostream&,constCylinder&);protected: floatheight;};//定义Cylinder类成员函数Cylinder::Cylinder(float
a,float
b,float
r,floath):Circle(a,b,r),height(h){}voidCylinder::setHeight(float
h){height=h;}floatCylinder::area()const{return2*Circle::area()+2*3.14159*radius*height;}floatCylinder::volume()const{returnCircle::area()*height;}ostream&operator<<(ostream&output,constCylinder&cy){ output<<″[″<<cy.x<<″,″<<cy.y<<″],r=″<<cy.radius<<″,h=″<<cy.height; returnoutput;}第(5)部分//main函数intmain(){Pointpoint(3.2,4.5);//建立Point类对象pointCirclecircle(2.4,1.2,5.5);//建立Circle类对象circleCylindercylinder(3.5,5.4,5.2,10.5);//建立Cylinder类对象cylinder
point.shapeName();//静态关联
cout<<point<<endl;
circle.shapeName();//静态关联
cout<<circle<<endl;
cylinder.shapeName();//静态关联
cout<<cylinder<<endl<<endl;Shape*pt;//定义基类指针
pt=&point;//指针指向Point类对象
pt->shapeName();
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 潜水装备数据采集与分析考核试卷
- 出售麦当劳店铺合同标准文本
- 出售积水别墅合同标准文本
- 兼职清洁人员合同标准文本
- 加盟合同标准文本餐饮加盟
- 制种劳务费合同标准文本
- 乙方解除施工合同标准文本
- 农药农膜化肥购销合同标准文本
- 代理销售大米合同标准文本
- 农村二层楼转让合同标准文本
- 2025年华能新能源股份有限公司广东分公司应届高校毕业生招聘笔试参考题库附带答案详解
- 公共场所安全知识课件
- 《临床诊断》课件-咳嗽
- DB32T 5013-2025镉污染耕地土壤减污修复黏土矿物-四氧化三铁-海藻酸钠基功能材料制备技术规程
- 体测免测申请书范文
- 介入手术术中安全护理措施
- 高中语文整本书阅读教学研究
- 2024年苏州农业职业技术学院高职单招语文历年参考题库含答案解析
- 投资银行学第4版- 课件汇 马晓军 第1-4章 投资银行概述-上市公司再融资
- 2025年月度工作日历含农历节假日电子表格版
- 中国近现代史纲要心得体会
评论
0/150
提交评论