C++核心编程——多态与虚函数
- 多态的概念
- 一个典型例子
- 利用虚函数实现动态多态性
- 虚函数的作用
- 虚析构函数
- 纯虚函数与抽象类
多态的概念
在面向对象方法中一般是这样表述多态性的:向不同的对象发送同一个消息,不同的对象在接收时会产生不同的行为(即方法)。
其实,我们已经多次接触过多态性的现象,例如函数的重载、运算符重载都是多态现象。只是那时没有用到多态性这一专门术语而已。
在C++中,多态性表现形式之一是:具有不同功能的函数可以用同一个函数名,这样就可以实现用一个函数名调用不同内容的函数。
系统实现的角度来看,多态性分为两类:静态多态性和动态多态性
- 静态多态:
通过函数重载实现的。由函数重载和运算符重载形成的多态性属于静态多态性,在程序编译时系统就能决定要调用的是哪个函数。静态多态性又称编译时的多态性。
静态多态性的函数调用速度快、效率高,但缺乏灵活性在程序运行前就已决定了执行的函数和方法。 - 动态多态:
不在编译时确定调用的是哪个函数,而是在程序运行过程中才动态地确定操作所针对的对象,具有很强的灵活性。它又称运行时的多态性。动态多态性是通过虚函数实现的。
一个典型例子
程序设计要求:
先建立一个Point(点)类,包含数据成员x,y(坐标点)。以它为基类,派生出一个Circle(圆)类,增加数据成员r(半径),再以Circle类为直接基类,派生出一个Cylinder(圆柱体)类,再增加数据成员h(高)。要求编写程序,重载运算符“<<”和”使之能用于输出以上类对象。
(1)声明基类Point类
class Point{
public:
//构造函数
Point(float tx, float ty):x(tx),y(ty){}
//功能函数声明
float getPX();
float getPY();
void setPoint(float tx, float ty);
//重载输出函数 做友元 输出点坐标
friend ostream & operator<<(ostream &output, Point &pt);
protected:
float x,y;
};
/*************Point 类函数定义 **************/
float Point::getPX(){
return x;
}
float Point::getPY(){
return y;
}
void Point::setPoint(float tx, float ty){
x = tx;
y = ty;
}
//重载输出函数 输出点坐标
ostream & operator<<(ostream &output, Point &pt)
{
output << "(" << + pt.getPX() << ", " << pt.getPY() << ")";
return output;
}
功能测试:
int main()
{
Point p1(1,1);
cout << p1 << endl;
p1.setPoint(2,3);
cout << p1 << endl;
return 0;
}
验证基类及函数的功能,核心关注/回顾“<<”运算符重载函数。
(2)声明派生类Circle类
//构造圆类
class Circle:public Point
{
public:
//构造函数
Circle(float tx, float ty, float tr):Point(tx,ty),r(tr){}
//功能函数声明
void setR(float tr);
float getR();
float getArea();
//重载输出函数 做友元 输出圆信息
friend ostream & operator<<(ostream &output, Circle &c);
protected:
float r;
};
/*************派生类Circle 功能函数定义**************/
void Circle::setR(float tr){
r = tr;
}
float Circle::getR(){
return r;
}
float Circle::getArea(){
return 3.14*r*r;
}
//重载输出函数 输出点圆的信息
ostream & operator<<(ostream &output, Circle &c)
{
output << "Center: (" << + c.getPX() << ", " << c.getPY() << ") , r: " << c.getR() << " area= " << c.getArea();
return output;
}
测试程序:
int main()
{
Point p1(1,1); //实例化一个点(2,2)
Circle c1(1, 1, 2); //实例化圆, 圆心(1,1) r=2
cout << c1 << endl;
c1.setPoint(2,2); //修改点坐标为(2,2);
c1.setR(4); //修改圆半径为 4
Point &pRef = c1; //基类点引用
//输出圆中点的信息
cout << "pRef: " << pRef << endl; //(2, 2)
return 0;
}
程序分析:
(1)在Point类中声明了一次运算符“<<”重载函数,在Circle类中又声明了一次运算符“<<”,两次重载的运算符“<<”内容是不同的,在编译时编译系统会根据输出项的类型确定调用哪一个运算符重载函数。
(2)注意main函数中的第8行
Point &pRef = c1;
定义了Point类的引用pRef,并用派生类Circle对象c1对其初始化。在继承与派生中提到,派生类对象可以替代基类对象向基类对象的引用初始化或赋值。
pRef 不能认为是c的别名,它只是c1 中基类部分的别名,得到了c1的起始地址,与c1中基类部分共享同一段存储单元。所以用“cout <<pRef”输出时调用的不是在Circle中声明的运算符重载函数,而是在Point中声明的运算符重载函数输出的是“点”的信息,而不是“圆”的信息。
输出结果:
Center: (1, 1) , r: 2 area= 12.56
pRef: (2, 2)
(3)声明Circle的派生类Cylinder
//圆柱体类
class Cylinder:public Circle
{
public:
//圆柱构造函数
Cylinder(float tx, float ty, float tr, float th): Circle(tx, ty, tr),h(th){}
//功能函数声明
void setH(float th);
float getH();
float getArea();
float getVolume();
//重载输出函数 做友元 输出圆柱体信息
friend ostream & operator<<(ostream &output, Cylinder &c);
protected:
float h;
};
/*************派生圆柱体类功能函数实现*************/
void Cylinder::setH(float th){
h = th;
}
float Cylinder::getH(){
return h;
}
float Cylinder::getArea(){
return 2*Circle::getArea() + 2*3.14*r*h;
}
float Cylinder::getVolume()
{
return (Circle::getArea() * h);
}
//重载运算符函数
ostream & operator<<(ostream &output, Cylinder &c)
{
output << "Center: (" << + c.getPX() << ", " << c.getPY() << ") , r: " << c.getR() << " h: " << c.getH();
output << "\narea= " << c.getArea() << "\t volume= " << c.getVolume();
return output;
}
测试函数
int main()
{
Point p1(1,1); //实例化一个点(1,1)
Circle c1(1, 1, 2); //实例化圆, 圆心(1,1) r=2
Cylinder cy1(1, 1, 2, 4); //实例化圆柱,底面圆心(1,1) 半径 2 高度 4
cout << "cy1 Info: " << cy1<< endl;
cy1.setPoint(2,2); //修改圆柱底面圆心为(2,2)
cy1.setR(3); //修改圆柱底面半径为 3
cy1.setH(5); //修改圆柱高为 5
cout << "new cylinder Info: " << cy1 << endl;
Point &pRef=cy1; //定义基类点的引用 输出圆柱中Point类的信息
cout << "pRef as point: " << pRef << endl;
Circle &cRef=cy1; //定义派生类 圆的引用 输出圆柱中Circle类的信息
cout << "cRef as point: " << cRef;
return 0;
}
程序分析:
- cy1.getArea()调用的是Cylinder类的getArea函数而不是Circle类的getArea函数。请注意这两个area函数不是重载,而是分别在基类和派生类中,属于同名覆盖。
- 注意测试程序中的点引用pRe是Point类型的引用,用cy1初始化,但pRef只是cy1基类Point部分的别名,以此在输出时也只输出点的信息。同理cRef是Circle类的引用,即使用cy1初始化,但在输出时只输出圆的信息。
输出结果:
cy1 Info: Center: (1, 1) , r: 2 h: 4
area= 75.36 volume= 50.24
new cylinder Info: Center: (2, 2) , r: 3 h: 5
area= 150.72 volume= 141.3
pRef as point: (2, 2)
cRef as point: Center: (2, 2) , r: 3 area= 28.26
利用虚函数实现动态多态性
虚函数的作用
在上述程序中,Circle和Cylinder类中都有getArea这个函数,这两个函数不仅名字相同,而且参数个数相同,但功能不同,函数体是不同的。这是合法的,因为它们不在同一个类中。编译系统按照同名覆盖的原则决定调用的对象。
通过指针调用,同一个语句pt->display();,可以调用不同派生层次中的 display 函数,只须在调用前临时给指针变量 pt 赋予不同的值(使之指向不同的类对象)即可。
典例: 声明两个类Student和Graduate有同样的方法display输出信息,通过指针访问不同类,输出不同信息。
这个例子开始时没有使用虚函数,然后再讨论使用虚函数的情况。
#include <iostream>
#include <string>
using namespace std;
//构造 Student类
class Student{
public:
Student(int n, string nam, float sco):num(n), name(nam), score(sco){}
void display(){
cout << "num: " << num << "\nname: " << name << "\nscore: " << score << endl;
}
protected:
int num;
string name;
float score;
};
//构造Graduate类
class Graduate:public Student
{
public:
Graduate(int n, string nam, float sco, float w):Student(n, nam, sco),wage(w){}
void display(){
Student::display();
cout << "wage: " << wage << endl;
}
protected:
float wage;
};
int main()
{
Student stu1(1001,"zhangsan",80);
Graduate grad1(1002,"lisi",95,600);
Student *pt = &stu1;
pt->display();
pt = &grad1;
pt->display();
return 0;
}
程序运行结果:
num: 1001
name: zhangsan
score: 80
num: 1002
name: lisi
score: 95
程序分析:
pt为Student类型的指针,即使指向grad1,也只能访问grad1基类数据,无法访问Graduate成员数据。
用虚函数就能顺利地解决这个问题。下面对程序作一点修改,在 Student 类中声明display函数时,在最左面加一个关键字virtual,即
virtual void display();
这样就把Student类的display 函数声明为虚函数。程序其他部分都不改动,再编译和运行就行正常输出Graduate类的数据。
用同一种调用形式pt->display(),而且pt是同一个基类指针,可以调用同一类族中不同类的虚函数。这就是多态性,对同一消息,不同对象有不同的响应方式。
虚函数的使用方法:
- 在基类中用virtual声明成员函数为虚函数。在类外定义虚函数时,不必再加virtual
- 在派生类中重新定义此函数,函数名、函数类型、函数参数个数和类型必须与基类的虚函数相同,根据派生类的需要重新定义函数体。
- 定义一个指向基类对象的指针变量,并使它指向同一类族中需要调用该函数的对象。
- 通过该指针变量调用此虚函数,此时调用的就是指针变量指向的对象的同名函数。
虚析构函数
#include <iostream>
using namespace std;
class Point
{
public:
Point(){}
//virtual ~Point()
~Point()
{
cout << "基类析构函数调用" << endl;
}
};
class Circle:public Point
{
public:
Circle(){}
~Circle(){
cout << "派生类虚构函数调用" << endl;
}
};
int main()
{
Point *pt = new Circle;
delete pt;
return 0;
}
程序分析:
pt是指向基类的指针变量,指向new开辟的动态存储空间,因此程序运行只执行基类的析构函数,如果希望能执行派生类 circle 的析构函数,需要将基类的析构函数声明为虚析构函数,如:
virtual ~Point();
最好把基类的析构函数声明为虚函数,这将使所有派生类的析构函数自动成为虚函数。
纯虚函数与抽象类
在第一个程序:一个典型的例子中,基类Point中没有求面积的getArea函数因为“点”是没有面积的,也就是说基类本身不需要这个函数。所以在Point类中没有定义getArea函数。但是,在其直接派生类Circle 和间接派生类Cylinder 中都需要有getArea 函数,而且这两个函数的功能不同,一个是求圆面积,另一个是求圆柱体表面积。我们自然会想到,在这种情况下应当将getArea声明为虚函数。可以在基类Point中加一个getArea函数并声明为虑函数:
virtual float getArea() const{
return 0;
}
其返回值为0,表示“点”是没有面积的。其实,在基类中并不使用这个函数。为了简化,可以不要写出这种无意义的函数体。只给出函数的原型,并在后面加上“=0”,如
virtual float getArea() const = 0;
这就将getArea声明为一个纯虚函数。纯虚函数是在声明虚函数时被“初始化”为0的函数。声明纯虚函数的一般形式是:
virtual 函数类型 函数名 (参数表列) = 0;
注意:
- 纯虚函数没有函数体
- 最后面的“=0”并不表示函数返回值为 0,它只起形式上的作用,告诉编译系统“这是纯虚函数”
- 这是一个声明语句,最后应有分号
- 纯虚函数不具备函数的功能,不能被调用,如要用,需要在派生类中重新定义。
纯虚函数的作用:
在基类中为其派生类保留一个函数的名字,以便派生类根据需要对它进行定义。如果在基类中没有保留函数名字,则无法实现多态性。
抽象类
如果声明了一个类,一般可以用它定义对象。但是在面向对象程序设计中,往往有一些类,它们不用来生成对象,目的是用它作为基类去建立派生类。
抽象类的派生类需要对基类中的纯虚函数进行定义,这个派生类就是可以用来定义对象的具体类。
虽然抽象类不能定义对象,但是可以定义指向抽象类数据的指针变量。当派生类成为具体类之后,就可以用这种指针指向派生类对象,然后通过该指针调用虚函数,实现多态性的操作。
应用实例
在一个典型例子中介绍了以Point 为基类的“点一圆一圆柱体”类的层次结构。现在程序中使用虚函数和抽象基类,并且类的层次结构的顶层是抽象基类 Shape(形状) Point(点)Circle(圆),Cylinder(圆柱体)都是 Shape 类的直接派生类和间接派生类。-
(1)声明抽象基类Shape
//声明抽象基类Shape class Shape { public: virtual float getArea() const {return 0.0;} //虚函数 求面积 virtual float getVolume() const {return 0.0;} //虚函数 virtual void shapeName() const = 0; //纯虚函数 };
-
(2)声明Point类
函数具体自定义实现参考上述一个典型例子的程序,该程序主要在原程序上进行对虚函数的重新定义//声明Point类 class Point:public Shape { public: //函数具体自定义实现参考上述一个典型例子程序 //构造函数 Point(float tx, float ty):x(tx),y(ty){} //功能函数声明 void setPoint(float tx, float ty); float getPX () const {return x;}; float getPY () const {return y;}; //对虚函数进行重新定义 virtual void shapeName() const {cout <<"Point: ";} //重载输出函数 做友元 输出点坐标 friend ostream & operator<<(ostream &output, const Point &pt); protected: float x,y; }; /*************Point 类函数定义 **************/ void Point::setPoint(float tx, float ty){ x = tx; y = ty; } //重载输出函数 输出点坐标 ostream & operator<<(ostream &output, const Point &pt) { output << "(" << + pt.x<< ", " << pt.y << ")"; return output; }
Point从Shape 继承了3个成员函数,由于“点”是没有面积和体积的因此不必重新定义getArea和getVolume,但Point类仍然从Shape类继承了这两个函数以便其派生类继承它们。shapeName函数在Shape类中是纯虚函数在Point类中要进行定义。Point类还有自己的成员函数(setPoint, getX,getY)和数据成员(x和y)。
-
(3)声明Circle类
函数具体自定义实现参考上述一个典型例子的程序,该程序主要在原程序上进行对getArea和shapeName虚函数的重新定义//构造圆类 class Circle:public Point { public: //构造函数 Circle(float tx, float ty, float tr):Point(tx,ty),r(tr){} //功能函数声明 void setR(float tr); //设定半径 float getR() const; //获取半径的值 //对虚函数进行再定义 virtual float getArea() const; virtual void shapeName() const {cout <<"Circle:";} //重载输出函数 做友元 输出圆信息 friend ostream & operator<<(ostream &output, const Circle &c); protected: float r; }; /*************派生类Circle 功能函数定义**************/ void Circle::setR(float tr){ r = tr; } float Circle::getR() const{ return r; } float Circle::getArea() const{ return 3.14*r*r; } //重载输出函数 输出点圆的信息 ostream & operator<<(ostream &output, const Circle &c) { output << "(" << + c.x << ", " << c.y<< ") , r: " << c.r; return output; }
-
(4)同理,声明Cylinder类
//圆柱体类 class Cylinder:public Circle { public: //圆柱构造函数 Cylinder(float tx, float ty, float tr, float th): Circle(tx, ty, tr),h(th){} //功能函数声明 void setH(float th); float getH(); //虚函数重定义 virtual float getArea() const; //重载虚函数 virtual float getVolume() const; virtual void shapeName() const {cout <<"Cylinder:";} //重载输出函数 做友元 输出圆柱体信息 friend ostream & operator<<(ostream &output, const Cylinder &c); protected: float h; }; /*************派生圆柱体类功能函数实现*************/ void Cylinder::setH(float th){ h = th; } float Cylinder::getH(){ return h; } float Cylinder::getArea() const{ return 2*Circle::getArea() + 2*3.14*r*h; } float Cylinder::getVolume() const{ return (Circle::getArea() * h); } //重载运算符函数 ostream & operator<<(ostream &output, const Cylinder &c) { output << "(" << + c.x << ", " << c.y << ") , r: " << c.r << " h: " << c.h; return output; }
-
(5)main函数
int main() { Point p1(3.2,4.5); //实例化一个点(3.2,4.5) Circle c1(2.4, 1.2, 5.6); //实例化圆, 圆心(2.4, 1.2) r = 5.6 Cylinder cy1(3.5, 6.4, 5.2, 10.5); //实例化圆柱,底面圆心(3.5, 6.4) 半径 5.2 高度 10.5 p1.shapeName(); //用对象名建立静态关联 cout << p1 << endl; //输出点的信息 c1.shapeName(); cout << c1 << endl; cy1.shapeName(); cout << cy1 << endl << endl; Shape *pt; //定义基类指针 pt = &p1; //使指针指向point对象 pt->shapeName(); //用指针建立动态关联 cout << "x=" << p1.getPX() << ", y=" << p1.getPY() << endl; cout << "area=" << pt->getArea() << "\nvolume=" << pt->getVolume() <<endl<< endl << endl; pt = &c1; //使指针指向point对象 pt->shapeName(); //用指针建立动态关联 cout << "x=" << c1.getPX() << ", y=" << c1.getPY() << endl; cout << "area=" << pt->getArea() << "\nvolume=" << pt->getVolume() <<endl<< endl << endl; pt = &cy1; //使指针指向point对象 pt->shapeName(); //用指针建立动态关联 cout << "x=" << cy1.getPX() << ", y=" << cy1.getPY() << endl; cout << "area=" << pt->getArea() << "\nvolume=" << pt->getVolume() <<endl<< endl << endl; return 0; }
程序分析:
首先先分别定义了Point类对象跑,Circle类对象从和Cylinder 类对象cy1。然后分别通过对象名p1,c1和cy1调用了shapeName 函数,这属于静态关联。
后续定义一个指向基类Shape对象的指针变量pt,使它先后指向3个派生类对象point,Circle和cylinder,然后通过指针调用各函数,如pt->shapeName(),pt ->getArea()等。这时是通过动态关联分别确定应该调用哪个函数,分别输出不同类对象的信息。
结果输出:
Point: (3.2, 4.5)
Circle:(2.4, 1.2) , r: 5.6
Cylinder:(3.5, 6.4) , r: 5.2 h: 10.5
Point: x=3.2, y=4.5
area=0
volume=0
Circle:x=2.4, y=1.2
area=98.4704
volume=0
Cylinder:x=3.5, y=6.4
area=512.699
volume=891.509