多态性(polymorphism)是面向对象程序设计的一个重要特征。利用多态性可以设计和实现一个易于扩展的系统。
在C++程序设计中,多态性是指具有不同功能的函数可以用同一个函数名,这样就可以用一个函数名调用不同内容的函数。在面向对象方法中一般是这样表述多态性的:向不同的对象发送同一个消息,不同的对象在接收时会产生不同的行为(即方法)。也就是说,每个对象可以用自己的方式去响应共同的消息。
在C++程序设计中,在不同的类中定义了其响应消息的方法,那么使用这些类时,不必考虑它们是什么类型,只要发布消息即可。从系统实现的角度看,多态性分为两类:静态多态性和动态多态性。
前边学过的函数重载和运算符重载实现的多态性属于静态多态性,在程序编译时系统就能决定调用的是哪个函数,因此静态多态性又称编译时的多态性。
静态多态性是通过函数的重载实现的(运算符重载实质上也是函数重载)。
动态多态性是在程序运行过程中才动态地确定操作所针对的对象。它又称运行时的多态性。
动态多态性是通过虚函数(virtual function)实现的。
有关静态多态性的应用已经介绍过了,这里主要介绍动态多态性和虚函数。
要研究的问题是:当一个基类被继承为不同的派生类时,各派生类可以使用与基类成员相同的成员名,如果在运行时用同一个成员名调用类对象的成员,会调用哪个对象的成员?也就是说,通过继承而产生了相关的不同的派生类,与基类成员同名的成员在不同的派生类中有不同的含义。也可以说,多态性是“一个接口,多种方法”。
下面是一个承上启下的例子。一方面它是有关继承和运算符重载内容的综合应用的例子,通过这个例子可以进一步融会贯通前面所学的内容,另一方面又是作为讨论多态性的一个基础用例。
例12.1 先建立一个Point(点)类,包含数据成员x,y(坐标点)。以它为基类,派生出一个Circle(圆)类,增加数据成员r(半径),再以Circle类为直接基类,派生出一个Cylinder(圆柱体)类,再增加数据成员h(高)。要求编写程序,重载运算符“<<”和“>>”,使之能用于输出以上类对象。
对于一个比较大的程序,应当分成若干步骤进行。先声明基类,再声明派生类,逐级进行,分步调试。声明基类Point类可写出声明基类Point的部分如下:
#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=a;y=b;} //对x,y初始化
void Point::setPoint(float a,float b) //设置x和y的坐标值
{x=a;y=b;} //为x,y赋新值
ostream & operator <<(ostream &output, const Point &p){output<<″[″<<p.x<<″,″<<p.y<<″]″<<endl; return output;}//重载运算符“<<”,使之能输出点的坐标
以上完成了基类Point类的声明。
现在要对上面写的基类声明进行调试,检查它是否有错,为此要写出main函数。实际上它是一个测试程序。
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点坐标
}
程序编译通过,运行结果为
x=3.5,y=6.4
p(new):[8.5,6.8]
测试程序检查了基类中各函数的功能,以及运算符重载的作用,证明程序是正确的。
(2)声明派生类Circle在上面的基础上,再写出声明派生类Circle的部分:
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类的定义,可以写出下面的主函数:
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;
}
程序编译通过,运行结果为
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] (输出圆的圆心“点”的数据)
(3)声明Circle的派生类Cylinder前面已从基类Point派生出Circle类,现在再从Circle派生出Cylinder类。
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;
} //重载运算符“<<”
可以写出下面的主函数:
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;
}
运行结果如下:
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作为一个“圆”输出)
在本例中存在静态多态性,这是运算符重载引起的。可以看到,在编译时编译系统即可以判定应调用哪个重载运算符函数。
在类的继承层次结构中,在不同的层次中可以出现名字相同、参数个数和类型都相同而功能不同的函数。编译系统按照同名覆盖的原则决定调用的对象。
在例12.1程序中用cy1.area( )调用的是派生类Cylinder中的成员函数area。如果想调用cy1中的直接基类Circle的area函数,应当表示为:cy1.Circle::area( )。用这种方法来区分两个同名的函数。
但是这样做很不方便。人们提出这样的设想,能否用同一个调用形式,既能调用派生类又能调用基类的同名函数。在程序中不是通过不同的对象名去调用不同派生层次中的同名函数,而是通过指针调用它们。
例如,用同一个语句“pt->display( );”可以调用不同派生层次中的display函数,只需在调用前给指针变量pt赋以不同的值(使之指向不同的类对象)即可。C++中的虚函数就是用来解决这个问题的。
虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。
请分析例12.2。这个例子开始时没有使用虚函数,然后再讨论使用虚函数的情况。
例12.2 基类与派生类中有同名函数。在下面的程序中Student是基类,Graduate是派生类,它们都有display这个同名的函数。
#include <iostream>
#include <string>
using namespace std;
//声明基类Student
class Student
{
public:Student(int, string,float);//声明构造函数void display( );//声明输出函数
protected ://受保护成员,派生类可以访问
int num;
string name;
float score;
};
//Student类成员函数的实现
Student::Student(int n, string nam,float s)//定义构造函数{num=n;name=nam;score=s;}
void Student::display( )//定义输出函数{cout<<″num:″<<num<<″\\nname:″<<name<<″\\nscore:″<<
在C++程序设计中,多态性是指具有不同功能的函数可以用同一个函数名,这样就可以用一个函数名调用不同内容的函数。在面向对象方法中一般是这样表述多态性的:向不同的对象发送同一个消息,不同的对象在接收时会产生不同的行为(即方法)。也就是说,每个对象可以用自己的方式去响应共同的消息。
在C++程序设计中,在不同的类中定义了其响应消息的方法,那么使用这些类时,不必考虑它们是什么类型,只要发布消息即可。从系统实现的角度看,多态性分为两类:静态多态性和动态多态性。
前边学过的函数重载和运算符重载实现的多态性属于静态多态性,在程序编译时系统就能决定调用的是哪个函数,因此静态多态性又称编译时的多态性。 静态多态性是通过函数的重载实现的(运算符重载实质上也是函数重载)。
动态多态性是在程序运行过程中才动态地确定操作所针对的对象。它又称运行时的多态性。 动态多态性是通过虚函数(virtual function)实现的。
有关静态多态性的应用已经介绍过了,这里主要介绍动态多态性和虚函数。
要研究的问题是:当一个基类被继承为不同的派生类时,各派生类可以使用与基类成员相同的成员名,如果在运行时用同一个成员名调用类对象的成员,会调用哪个对象的成员?也就是说,通过继承而产生了相关的不同的派生类,与基类成员同名的成员在不同的派生类中有不同的含义。也可以说,多态性是“一个接口,多种方法”。
下面是一个承上启下的例子。一方面它是有关继承和运算符重载内容的综合应用的例子,通过这个例子可以进一步融会贯通前面所学的内容,另一方面又是作为讨论多态性的一个基础用例。
例12.1 先建立一个Point(点)类,包含数据成员x,y(坐标点)。以它为基类,派生出一个Circle(圆)类,增加数据成员r(半径),再以Circle类为直接基类,派生出一个Cylinder(圆柱体)类,再增加数据成员h(高)。要求编写程序,重载运算符“<<”和“>>”,使之能用于输出以上类对象。
对于一个比较大的程序,应当分成若干步骤进行。先声明基类,再声明派生类,逐级进行,分步调试。声明基类Point类可写出声明基类Point的部分如下:
#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=a;y=b;} //对x,y初始化
void Point::setPoint(float a,float b) //设置x和y的坐标值
{x=a;y=b;} //为x,y赋新值
ostream & operator <<(ostream &output, const Point &p){output<<″[″<<p.x<<″,″<<p.y<<″]″<<endl; return output;}//重载运算符“<<”,使之能输出点的坐标
以上完成了基类Point类的声明。
现在要对上面写的基类声明进行调试,检查它是否有错,为此要写出main函数。实际上它是一个测试程序。
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点坐标
}
程序编译通过,运行结果为
x=3.5,y=6.4
p(new):[8.5,6.8]
测试程序检查了基类中各函数的功能,以及运算符重载的作用,证明程序是正确的。
(2)声明派生类Circle在上面的基础上,再写出声明派生类Circle的部分:
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类的定义,可以写出下面的主函数:
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;
}
程序编译通过,运行结果为
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] (输出圆的圆心“点”的数据)
(3)声明Circle的派生类Cylinder前面已从基类Point派生出Circle类,现在再从Circle派生出Cylinder类。
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;
} //重载运算符“<<”
可以写出下面的主函数:
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;
}
运行结果如下:
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作为一个“圆”输出)
在本例中存在静态多态性,这是运算符重载引起的。可以看到,在编译时编译系统即可以判定应调用哪个重载运算符函数。
在类的继承层次结构中,在不同的层次中可以出现名字相同、参数个数和类型都相同而功能不同的函数。编译系统按照同名覆盖的原则决定调用的对象。
在例12.1程序中用cy1.area( )调用的是派生类Cylinder中的成员函数area。如果想调用cy1中的直接基类Circle的area函数,应当表示为:cy1.Circle::area( )。用这种方法来区分两个同名的函数。
但是这样做很不方便。人们提出这样的设想,能否用同一个调用形式,既能调用派生类又能调用基类的同名函数。在程序中不是通过不同的对象名去调用不同派生层次中的同名函数,而是通过指针调用它们。
例如,用同一个语句“pt->display( );”可以调用不同派生层次中的display函数,只需在调用前给指针变量pt赋以不同的值(使之指向不同的类对象)即可。C++中的虚函数就是用来解决这个问题的。
虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。
请分析例12.2。这个例子开始时没有使用虚函数,然后再讨论使用虚函数的情况。
例12.2 基类与派生类中有同名函数。在下面的程序中Student是基类,Graduate是派生类,它们都有display这个同名的函数。
#include <iostream>
#include <string>
using namespace std;
//声明基类Student
class Student
{
public:Student(int, string,float);//声明构造函数void display( );//声明输出函数
protected ://受保护成员,派生类可以访问
int num;
string name;
float score;
};
//Student类成员函数的实现
Student::Student(int n, string nam,float s)//定义构造函数{num=n;name=nam;score=s;}
void Student::display( )//定义输出函数{cout<<″num:″<<num<<″\\nname:″<<name<<″\\nscore:″<<