多态性(polymorphism)是面向对象程序设计的一个重要特征。如果一种语言只支持类而不支持多态,是不能被称为面向对象语言的,只能说是基于对象的,如Ada、VB就属此类。C++支持多态性,在C++程序设计中能够实现多态性。利用多态性可以设计和实现一个易于扩展的系统。
顾名思义,多态的意思是一个事物有多种形态。多态性的英文单词polymorphism来源于希腊词根poly(意为“很多”)和morph(意为“形态”)。在C ++程序设计中,多态性是指具有不同功能的函数可以用同一个函数名,这样就可以用一个函数名调用不同内容的函数。在面向对象方法中一般是这样表述多态性的:向不同的对象发送同一个消息, 不同的对象在接收时会产生不同的行为(即方法)。也就是说,每个对象可以用自己的方式去响应共同的消息。所谓消息,就是调用函数,不同的行为就是指不同的实现,即执行不同的函数。
其实,我们已经多次接触过多态性的现象,例如函数的重载、运算符重载都是多态现象。只是那时没有用到多态性这一专门术语而已。例如,使用运算符“+”使两个数值相加,就是发送一个消息,它要调用operator +函数。实际上,整型、单精度型、双精度型的加法操作过程是互不相同的,是由不同内容的函数实现的。显然,它们以不同的行为或方法来响应同一消息。
在现实生活中可以看到许多多态性的例子。如学校校长向社会发布一个消息:9月1日新学年开学。不同的对象会作出不同的响应:学生要准备好课本准时到校上课;家长要筹集学费;教师要备好课;后勤部门要准备好教室、宿舍和食堂……由于事先对各种人的任务已作了规定,因此,在得到同一个消息时,各种人都知道自己应当怎么做,这就是 多态性。可以设想,如果不利用多态性,那么校长就要分别给学生、家长、教师、后勤部门等许多不同的对象分别发通知,分别具体规定每一种人接到通知后应该怎么做。显然这是一件十分复杂而细致的工作。一人包揽一切,吃力还不讨好。现在,利用了多态性机制,校长在发布消息时,不必一一具体考虑不同类型人员是怎样执行的。至于各类人员在接到消息后应气做什么,并不是临时决定的,而是学校的工作机制事先安排决定好的。校长只需不断发布各种消息,各种人员就会按预定方案有条不紊地工作。
同样,在C++程序设计中,在不同的类中定义了其响应消息的方法,那么使用这些类 时,不必考虑它们是什么类型,只要发布消息即可。正如在使用运算符“ ”时不必考虑相加的数值是整型、单精度型还是双精度型,直接使用“+”,不论哪类数值都能实现相加。可以说这是以不变应万变的方法,不论对象千变万化,用户都是用同一形式的信息去调用它们,使它们根据事先的安排作出反应。
从系统实现的角度看,多态性分为两类:静态多态性和动态多态性。以前学过的函数重载和运算符重载实现的多态性属于静态多态性,在程序编译时系统就能决定调用的是哪个函数,因此静态多态性又称编译时的多态性。静态多态性是通过函数的重载实现的(运算符重载实质上也是函数重载)。动态多态性是在程序运行过程中才动态地确定操作所针对的对象。它又称运行时的多态性。动态多态性是通过虚函数(Virtual fiinction)实现的。
下面是一个承上启下的例子。一方面它是有关继承和运算符重载内容的综合应用的例子,通过这个例子可以进一步融会贯通前面所学的内容,另一方面又是作为讨论多态性的一个基础用例。
希望大家耐心、深入地阅读和消化这个程序,弄清其中的每一个细节。
[例] 先建立一个Point(点)类,包含数据成员x,y(坐标点)。以它为基类,派生出一个Circle(圆)类,增加数据成员r(半径),再以Circle类为直接基类,派生出一个Cylinder(圆柱体)类,再增加数据成员h(高)。要求编写程序,重载运算符“<<”和“>>”,使之能用于输出以上类对象。
这个例题难度不大,但程序很长。对于一个比较大的程序,应当分成若干步骤进行。先声明基类,再声明派生类,逐级进行,分步调试。
1) 声明基类Point
类可写出声明基类Point的部分如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
#include <iostream> //声明类Point class Point { public : 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类的成员函数 Point::Point( float a, float b) //Point的构造函数 { //对x,y初始化 x=a; y=b; } void Point::setPoint( float a, float b) //设置x和y的坐标值 { //为x,y赋新值 x=a; y=b; } //重载运算符“<<”,使之能输出点的坐标 ostream & operator <<(ostream &output, const Point &p) { output<< "[" <<p.x<< "," <<p.y<< "]" <<endl; return output; } |
以上完成了基类Point类的声明。
为了提高程序调试的效率,提倡对程序分步调试,不要将一个长的程序都写完以后才统一调试,那样在编译时可能会同时出现大量的编译错误,面对一个长的程序,程序人员往往难以迅速准确地找到出错位置。要善于将一个大的程序分解为若干个文件,分别编译,或者分步调试,先通过最基本的部分,再逐步扩充。
现在要对上面写的基类声明进行调试,检查它是否有错,为此要写出main函数。实际上它是一个测试程序。
1
2
3
4
5
6
7
8
|
int main( ) { Point p(3.5,6.4); //建立Point类对象p cout<< "x=" <<p.getX( )<< ",y=" <<p.getY( )<<endl; //输出p的坐标值 p.setPoint(8.5,6.8); //重新设置p的坐标值 cout<< "p(new):" <<p<<endl; //用重载运算符“<<”输出p点坐标 return 0; } |
getX和getY函数声明为常成员函数,作用是只允许函数引用类中的数据,而不允许修改它们,以保证类中数据的安全。数据成员x和y声明为protected,这样可以被派生类访问(如果声明为private,派生类是不能访问的)。
程序编译通过,运行结果为:
1
2
|
x=3.5,y=6.4 p(new):[8.5,6.8] |
测试程序检查了基类中各函数的功能,以及运算符重载的作用,证明程序是正确的。
2)声明派生类Circle
在上面的基础上,再写出声明派生类Circle的部分:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
class 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 ; //计算圆面积 friend ostream &operator <<(ostream &, const Circle &); //重载运算符“<<” private : 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;} //计算圆面积 float Circle::area( ) const { return 3.14159*radius*radius; } //重载运算符“<<”,使之按规定的形式输出圆的信息 ostream &operator <<(ostream &output, const Circle &c) { output<< "Center=[" <<c.x<< "," <<c.y<< "],r=" <<c.radius<< ",area=" <<c.area( )<<endl; return output; } |
为了测试以上Circle类的定义,可以写出下面的主函数:
1
2
3
4
5
6
7
8
9
10
11
|
int main( ) { Circle c(3.5,6.4,5.2); //建立Circle类对象c,并给定圆心坐标和半径 cout<< "original circle:\\nx=" <<c.getX()<< ", y=" <<c.getY()<< ", r=" <<c.getRadius( )<< ", area=" <<c.area( )<<endl; //输出圆心坐标、半径和面积 c.setRadius(7.5); //设置半径值 c.setPoint(5,5); //设置圆心坐标值x,y cout<< "new circle:\\n" <<c; //用重载运算符“<<”输出圆对象的信息 Point &pRef=c; //pRef是Point类的引用变量,被c初始化 cout<< "pRef:" <<pRef; //输出pRef的信息 return 0; } |
程序编译通过,运行结果为:
1
2
3
4
5
|
original circle:(输出原来的圆的数据) x=3.5, y=6.4, r=5.2, area=84.9486 new circle:(输出修改后的圆的数据) Center=[5,5], r=7.5, area=176.714 pRef:[5,5] (输出圆的圆心“点”的数据) |
可以看到,在Point类中声明了一次运算符“ <<”重载函数,在Circle类中又声明了一次运算符“ <<”,两次重载的运算符“<<”内容是不同的,在编译时编译系统会根据输出项的类型确定调用哪一个运算符重载函数。main函数第7行用“cout<< ”输出c,调用的是在Circle类中声明的运算符重载函数。
请注意main函数第8行:
1
|
Point & pRef = c; |
定义了 Point类的引用变量pRef,并用派生类Circle对象c对其初始化。前面我们已经讲过,派生类对象可以替代基类对象为基类对象的引用初始化或赋值(详情请查看:C++基类与派生类的转换)。现在 Circle是Point的公用派生类,因此,pRef不能认为是c的别名,它得到了c的起始地址, 它只是c中基类部分的别名,与c中基类部分共享同一段存储单元。所以用“cout<<pRef”输出时,调用的不是在Circle中声明的运算符重载函数,而是在Point中声明的运算符重载函数,输出的是“点”的信息,而不是“圆”的信息。
3) 声明Circle的派生类Cylinder
前面已从基类Point派生出Circle类,现在再从Circle派生出Cylinder类。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
class Cylinder: public Circle // Cylinder是Circle的公用派生类 { public : Cylinder ( float x=0, float y=0, float r=0, float h=0); //构造函数 void setHeight( float ); //设置圆柱高 float getHeight( ) const ; //读取圆柱高 loat area( ) const ; //计算圆表面积 float volume( ) const ; //计算圆柱体积 friend ostream& operator <<(ostream&, const Cylinder&); //重载运算符<< protected : float height; //圆柱高 }; //定义构造函数 Cylinder::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() const { return Circle::area()*height;} ostream &operator <<(ostream &output, const Cylinder& cy) { output<< "Center=[" <<cy.x<< "," <<cy.y<< "],r=" <<cy.radius<< ",h=" <<cy.height << "\\narea=" <<cy.area( )<< ", volume=" <<cy.volume( )<<endl; return output; } //重载运算符“<<” |
可以写出下面的主函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
int main( ) { Cylinder cy1(3.5,6.4,5.2,10); //定义Cylinder类对象cy1 cout<< "\\noriginal 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 cout<< "\\nnew cylinder:\\n" <<cy1; //用重载运算符“<<”输出cy1的数据 Point &pRef=cy1; //pRef是Point类对象的引用变量 cout<< "\\npRef as a Point:" <<pRef; //pRef作为一个“点”输出 Circle &cRef=cy1; //cRef是Circle类对象的引用变量 cout<< "\\ncRef as a Circle:" <<cRef; //cRef作为一个“圆”输出 return 0; } |
运行结果如下:
1
2
3
4
5
6
7
8
|
original cylinder:(输出cy1的初始值) x=3.5, y=6.4, r=5.2, h=10 (圆心坐标x,y。半径r,高h) area=496.623, volume=849.486 (圆柱表面积area和体积volume) new cylinder: (输出cy1的新值) Center=[5,5], r=7.5, h=15 (以[5,5]形式输出圆心坐标) area=1060.29, volume=2650.72(圆柱表面积area和体积volume) pRef as a Point:[5,5] (pRef作为一个“点”输出) cRef as a Circle:Center=[5,5], r=7.5, area=176.714(cRef作为一个“圆”输出) |
说明:在Cylinder类中定义了 area函数,它与Circle类中的area函数同名,根据前面我们讲解的同名覆盖的原则(详情请查看:C++多重继承的二义性问题),cy1.area( ) 调用的是Cylinder类的area函数(求圆柱表面积),而不是Circle类的area函数(圆面积)。请注意,这两个area函数不是重载函数,它们不仅函数名相同,而且函数类型和参数个数都相同,两个同名函数不在同 —个类中,而是分别在基类和派生类中,属于同名覆盖。重载函数的参数个数和参数类型必须至少有一者不同,否则系统无法确定调用哪一个函数。
main函数第9行用“cout<<cy1”来输出cy1,此时调用的是在Cylinder类中声明的重载运算符“<<”,按在重载时规定的方式输出圆柱体cy1的有关数据。
main函数中最后4行的含义与在定义Circle类时的情况类似。pRef是Point类的引用变量,用cy1对其初始化,但它不是cy1的别名,只是cy1中基类Point部分的别名,在输出pRef时是作为一个Point类对象输出的,也就是说,它是一个“点”。同样,cRef是Circle类的引用变量,用cy1对其初始化,但它只是cy1中的直接基类Circle部分的别名, 在输出 cRef 时是作为Circle类对象输出的,它是一个"圆”,而不是一个“圆柱体”。从输 出的结果可以看出调用的是哪个运算符函数。
在本例中存在静态多态性,这是运算符重载引起的(注意3个运算符函数是重载而不是同名覆盖,因为有一个形参类型不同)。可以看到,在编译时编译系统即可以判定应调用哪个重载运算符函数。