多态性与虚函数
多态性的概念
- 多态性(polymorphism)是面向对象程序设计的一个重要特征。如果一种语言只支持类而不支持多态,是不能称为面向对象语言的,只能说是基于对象的。
- 在面向对象方法中一般是这样表述多态性的:向不同的对象发送同一个消息,不同的对象在接收时会产生不同的行为(即方法)。所谓消息,就是调用函数,不同的行为就是指不同的实现,即执行不同的函数。
- 在C++中,多态性表现形式之一是:具有不同功能的函数可以用同一个函数名,这样就可以实现用一个函数名调用不同内容的函数。可以说,多态性是"一个接口,多种方法"。不论对象千变万化,用户都是用同一形式的信息去调用它们,使它们根据事先的安排作出反应。
从系统实现的角度来看,多态性分为两类:静态多态性和动态多态性。
- 静态多态性:通过函数重载实现的。由函数重载和运算符重载(运算符重载实质上也是函数重载)形成的多态性属于静态多态性,要求在程序编译时就知道调用函数的全部信息,因此,在程序编译时系统就决定要调用的是哪个函数。静态多态性又称为编译时的多态性,静态多态性的函数调用速度快,效率高,但缺乏灵活性,在程序运行前就已决定了执行的函数和方法。
- 动态多态性:不在编译时确定调用的是哪个函数,而是在程序运行过程中才动态地确定操作所针对的对象。它又称运行时的多态性。动态多态性是通过虚函数(virtual function)来实现的。
例:先建立一个Point(点)类,包含数据成员x,y(坐标点)。以它为基类,派生出一个Circle(圆)类,增加数据成员r(半径),再以Circle类为直接基类,派生出一个Cylinder(圆柱体)类,再增加数据成员h(高)。要求编写程序,重载运算符"<<“和”>",使之能用于输出以上类对象
#include<iostream>
using namespace std;
class Point //声明类Point
{
public:
Point(float x = 0, float y = 0); //有默认参数的构造函数
void setPoint(float, float); //设置坐标值
float getX() const { return x; } //读x坐标,getX函数为常成员函数
float getY() const { return y; } //读y坐标,getY函数为常成员函数
friend ostream& operator<<(ostream&, const Point&); //友元重载运算符<<
protected: //受保护成员
float x, y;
};
//下面是定义Point类的成员函数
//Point的构造函数
Point::Point(float a, float b) { x = a; y = b; } //对x,y初始化
//设置x和y的坐标值
void Point::setPoint(float a, float b) { x = a; y = b; } //对x,y赋予初值
//重载运算符"<<",使之能输出点的坐标
ostream& operator<<(ostream& output, const Point& p)
{
output << "[" << p.x << "," << p.y << "]" << endl;
return output;
}
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&); //重载运算符"<<"
protected:
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;
}
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; //读取圆柱高
float 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()
{
Point p(3.5, 6.4); //建立Point类对象p,对x,y初始化
cout << "x=" << p.getX() << ",y=" << p.getY() << endl; //输出p的坐标值x,y
p.setPoint(8.5, 6.8); //重新设置p的坐标值
cout << "p(new):" << p << endl; //用重载运算符"<<"输出p点坐标
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 << endl << "new circle:\n" << c; //用重载运算符"<<"输出圆对象的信息
Point& pRef = c; //pRef是Point类的引用,被c初始化
cout << "pRef:" << pRef; //输出pRed的信息
Cylinder cy1(3.5, 6.4, 5.2, 10); //定义Cylinder类对象cy1,并初始化
//用系统定义的运算符"<<"输出cy1的数据
cout << endl << "original cylinder:\nx=" << cy1.getX() << ",y=" << cy1.getY() << ",r=" << cy1.getRadius() <<
",h=" << cy1.getHeight() << "\narea=" << cy1.area() << ",volume=" << cy1.volume() << endl;
cy1.setHeight(15); //设置圆柱高
cy1.setRadius(7.5); //设置圆半径
cy1.setPoint(5, 5); //设置圆心坐标值x,y
cout << "\nnew cylinder:\n" << cy1; //用重载运算符"<<"输出cy1的数据
pRef = cy1; //pRef是Point类对象的引用
cout << "\npRef as a point:" << pRef; //pRef作为一个"点"输出
Circle& cRef = cy1; //cRef圆Circle类对象的引用
cout << "cRef as a Circle:" << cRef; //cRef作为一个"圆"输出
return 0;
}
程序分析:
- getX和getY函数声明为常成员函数,作用是只允许函数引用类中的数据,而不允许修改它们,以保证类中数据的安全。数据成员x和y声明为protected,这样可以被派生类访问(如果声明为private,派生类是不能访问的)。
- 在Point类中声明了一次运算符"<<“重载函数,在Circle类中又声明了一次运算符” <<",两次重载的运算符"<<"内容是不同的,在编译时编译系统会根据输出项的类型确定调用哪一个运算符重载函数。
- 定义了 Point类的引用变量pRef,并用派生类Circle对象c对其初始化。派生类对象可以替代基类对象为基类对象的引用初始化或赋值。现在Circle是Point的公用派生类,因此,pRef不能认为是c的别名,它得到了c的起始地址, 它只是c中基类部分的别名,与c中基类部分共享同一段存储单元。所以用"cout<<pRef"输出时,调用的不是在Circle中声明的运算符重载函数,而是在Point中声明的运算符重载函数,输出的是"点"的信息,而不是"圆"的信息。
- 在Cylinder类中定义了area函数,它与Circle类中的area函数同名,根据同名覆盖的原则,cy1.area( )调用的是Cylinder类的area函数(求圆柱表面积),而不是Circle类的area函数(圆面积)。请注意,这两个area函数不是重载函数,它们不仅函数名相同,而且函数类型和参数个数都相同,两个同名函数不在同一个类中,而是分别在基类和派生类中,属于同名覆盖。重载函数的参数个数和参数类型必须至少有一者不同,否则系统无法确定调用哪一个函数。
运行结果:
利用虚函数实现动态多态性
虚函数的作用
- 在同一类中是不能定义两个名字相同、参数个数和类型相同的函数的,否则就是"重复定义"。但是在类的继承层次结构中,在不同的层次中可以出现名字相同、参数个数和类型都相同而功能不同的函数。编译系统按照同名覆盖的原则决定调用的对象。
- C++中的虚函数就是用来解决动态多态问题的。所谓虚函数,就是在基类声明函数是虚拟的,并不是实际存在的,然后在派生类中才正式定义此函数。在程序运行期间,用指针指向某一派生类对象,这样就能调用指针指向的派生类对象中的函数,而不会调用其他派生类中的函数。
- 注意:虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。
例:基类与派生类中有同名函数
#include<iostream>
#include<string>
using namespace std;
class Student //声明基类
{
public:
Student(int, string, float); //声明构造函数
void display(); //声明输出函数
protected: //受保护成员,派生类可以访问
int num;
string name;
float score;
};
Student::Student(int n,string nam,float s) //定义构造函数
{
num = n;
name = nam;
score = s;
}
void Student::display() //定义输出函数
{
cout << "num:" << num << "\nname:" << name << "\nscore:" << score << "\n\n";
}
class Graduate :public Student
{
public:
Graduate(int, string, float, float); //声明构造函数
void display(); //与基类的输出函数同名
private:
float wage;
};
Graduate::Graduate(int n, string nam, float s, float w) :Student(n, nam, s), wage(w) {}
void Graduate::display() //定义输出函数
{
cout << "num:" << num << "\nname:" << name << "\nscore:" << score << "\nwage=" << wage << endl;
}
int main()
{
Student stud1(1001, "Li", 87.5); //定义Student类对象stud1
Graduate grad1(2001, "Wang", 98.5, 1200); //定义Graduate类对象grad1
Student* pt = &stud1; //定义指向基类对象的指针变量pt,指向stud1
pt->display(); //输出Student(基类)对象stud1中的数据
pt = &grad1; //pt指向Graduate类对象grad1
pt->display(); //希望输出Graduate类对象grad1中的数据
return 0;
}
程序分析:
- Student类中的display函数的作用和Graduate类中的display函数的作用是不同的。在主函数中定义了指向基类对象的指针变量pt,并先使pt指向stud1,用pt->display( )输出基类对象stud1的全部数据成员,然后使pt指向grad1,再调用pt->display( ),试图输出grad1的全部数据成员,但实际上只输出了grad1中的基类的数据成员,说明它并没有调用grad1中的display函数,而是调用了stud1中的display函数。
- 假如想输出grad1的全部数据成员,通过对象名调用display函数,如
gradl.display()
,或者定义一个指向Graduate类对象的指针ptr,然后使ptr指向grad1,再用ptr->display()
调用。
运行结果:
- 用虚函数也能顺利地解决这个问题,在Student类中声明display函数时,在最左面加上一个关键字virtual,即
virtual void display();
- 运行结果:
- 现在用同一个指针变量(指向基类对象的指针变量),不但输出了学生stud1的全部数据,而且还输出了研究生grad1的全部数据,说明已调用了grad1的display函数。用同一种调用形式
pt->display()
,而且pt是同一个基类指针,可以调用同一类族中不同类的虚函数。这就是多态性,对同一消息,不同对象有不同的响应方式。- 说明:本来基类指针是用来指向基类对象的,如果用它指向派生类对象,则进行指针类型转换,将派生类对象的指针先转换为基类的指针,所以基类指针指向的是派生类对象中的基类部分。在程序修改前,是无法通过基类指针去调用派生类对象中的成员函数的。
- 虚函数突破了这一限制,在基类中的display被声明为虚函数,在声明派生类时被重载,这时派生类的同名函数display就取代了其基类中的虚函数。因此在使基类指针指向派生类对象后,调用display函数时就调用了派生类的display函数。要注意的是,只有用virtual声明了虚函数后才具有以上作用。如果不声明为虚函数,企图通过基类指针调用派生类的非虚函数是不行的。
- 可以看到:当把基类的某个成员函数声明为虚函数后,允许在其派生类中对该函数重新定义,赋予它新的功能,并且可以通过指向基类的指针指向同一类族中不同类的对象,从而调用其中的同名函数。
- 注意:由虚函数实现的动态多态性就是:同一类族中不同类的对象,对同一函数调用作出不同的响应。
虚函数的使用方法是:
- 在基类用virtual声明成员函数为虚函数。在类外定义虚函数时,不必再加virtual
- 在派生类中重新定义此函数,要求函数名、函数类型、函数参数个数和类型必须与基类的虚函数相同,根据派生类的需要重新定义函数体
- 当一个成员函数被声明为虚函数后,其派生类中的同名函数都自动成为虚函数。因此在派生类重新声明该虚函数时,可以加virtual,也可以不加,但习惯上一般在每一层声明该函数时都加virtual,使程序更加清晰。
- 如果在派生类中没有对基类的虚函数重新定义,则派生类简单地继承其直接基类的虚函数。
- 定义一个指向基类对象的指针变量,并使它指向同一类族中需要调用该函数的对象
- 通过该指针变量调用此虚函数,此时调用的就是指针变量指向的对象的同名函数
需要说明:有时在基类中定义的非虚函数会在派生类中被重新定义,如果用基类指针调用该成员函数,则系统会调用对象中基类部分的成员函数;如果用派生类指针调用该成员函数,则系统会调用派生类对象中的成员函数,这并不是多态性行为(使用的是不同类型的指针),没有用到虚函数的功能。
以前介绍的函数重载处理的是同一层次上的同名函数问题,而虚函数处理的是不同派生层次上的同名函数问题,前者是横向重载,后者可以理解为纵向重载。但与重载不同的是:同一类族的虚函数的首部是相同的,而函数重载时函数的首部是不同的(参数个数或类型不同)。
静态关联与动态关联
- 确定调用的具体对象的过程称为关联,这里指把一个函数名与一个类对象捆绑在一起,建立关联。
- 函数重载和通过对象名调用的虚函数,在编译时即可确定其调用的虚函数属于哪一个类,其过程称为静态关联(static binding),由于是在运行前进行关联的,所以又称为早期关联(early binding),函数重载属于静态关联。
- 在运行阶段把虚函数和类对象"绑定"在一起的过程称为动态关联(dynamic binding),这种多态性是动态的多态性,即运行阶段的多态性。
- 在运行阶段,指针可以先后指向不同的类对象,从而调用同一类族中不同类的虚函数。由于动态关联是在编译以后的运行阶段进行的,因此也称为滞后关联(late binding)。