利用虚函数实现动态多态性
在什么情况下应当声明虚函数
使用虚函数时,有两点要注意:
- 只能用virtual声明类的成员函数,使它成为虚函数,而不能将类外的普通函数声明为虚函数。因为虚函数的作用是允许在派生类中对基类的虚函数重新定义。显然,它只能用于类的继承层次结构中。
- 一个成员函数被声明为虚函数后,在同一类族中的类就不能再定义一个非virtual的但与该虚函数具有相同的参数(包括个数和类型)和函数返回值类型的同名函数。
根据什么考虑是否把一个成员函数声明为虚函数呢?主要考虑以下几点:
- 首先看成员函数所在的类是否会作为基类。然后看成员函数在类的继承后有无可能被更改功能,如果希望更改其功能的,一般应该将它声明为虚函数。
- 如果成员函数在类被继承后功能不需修改,或派生类用不到该函数,则不要把它声明为虚函数。不要仅仅考虑到要作为基类而把类中的所有成员函数都声明为虚函数。
- 应考虑对成员函数的调用是通过对象名还是通过基类指针或引用去访问,如果是通过基类指针或引用去访问的,则应当声明为虚函数。
- 有时,在定义虚函数时,并不定义其函数体,即函数体是空的。它的作用只是定义了一个虚函数名,具体功能留给派生类去添加。
需要说明的是:使用虚函数,系统要有一定的空间开销。当一个类带有虚函数时,编译系统会为该类构造一个虚函数表(virtual function table,简称vtable),它是一个指针数组,存放每个虚函数的入口地址。系统在进行动态关联时的时间开销是很少的,因此,多态性是高效的。
虚析构函数
- 析构函数的作用是在对象撤销之前做必要的"清理现场"的工作。当派生类的对象从内存中撤销时一般先调用派生类的析构函数,然后再调用基类的析构函数。但是,如果用new运算符建立了临时对象,若基类中有析构函数,并且定义了一个指向该基类的指针变量。在程序用带指针参数的delete运算符撤销对象时,会发生一个情况:系统会只执行基类的析构函数,而不执行派生类的析构函数。
例:基类中有非虚构函数时的执行情况
#include <iostream>
using namespace std;
class Point //定义基类Point类
{
public:
Point() {} //Point类构造函数
~Point() { cout << "executing Point destructor" << endl; } //Point类析构函数
};
class Circle :public Point //定义派生类Circle类
{
public:
Circle() {} //Circle类构造函数
~Circle() { cout << "executing Circle destructor" << endl; } //Circle类析构函数
private:
int radus;
};
int main()
{
Point* p = new Circle; //用new开辟Circle类对象的动态存储字间
delete p; //用delete释放动态存储空间
return 0;
}
运行结果:
executing Point destructor
程序分析:
- p是指向基类Point的指针变量,指向new开辟的动态存储空间。希望用detele释放p所指向的空间。但运行结果为:
executmg Point destmctor
,表示只执行了基类Point的析构函数,而没有执行派生类Circle的析构函数。- 如果希望能执行派生类Circle的析构函数,可以将基类的析构函数声明为虚析构函数, 如:
virtual ~Point() { cout << "executing Point destructor" << endl; }
- 当基类的析构函数为虚函数时,无论指针指的是同一类族中的哪一个类对象,当对象撤销时,系统会采用动态关联,调用相应的析构函数,对该对象进行清理工作。
- 如果将基类的析构函数声明为虚函数,由该基类所派生的所有派生类的析构函数也都自动成为虚函数,即使派生类的析构函数与基类的析构函数名字不相同。
- 在程序中最好把基类的析构函数声明为虚函数。这将使所有派生类的析构函数自动成为虚函数。这样,如果程序中显式地用delete运算符准备删除一个对象,而delete运算符的操作对象用了指向派生类对象的基类指针,则系统会调用相应类的析构函数。
构造函数不能声明为虚函数。这是因为在执行构造函数时类对象还未完成建立过程,当然谈不上函数与类对象的关联。
纯虚函数与抽象类
纯虚函数
- 纯虚函数是在声明虚函数时被"初始化"为0的函数
- 声明虚函数的一般形式是:
virtual 函数类型 类型名(参数表列) = 0;
注意:
- 纯虚函数没有函数体
- 最后面的"=0"并不表示函数返回值为0,它只起形式上的作用,告诉编译系统“这是纯虚函数”
- 这是一个声明语句,最后应有分号
- 纯虚函数只有函数的名字而不具备函数的功能,不能被调用。它只是通知编译系统:"在这里声明一个虚函数,留待派生类中定义:。在派生类中对此函数提供定义后,它才能具备函数的功能,可被调用
- 纯虚函数的作用是在基类中为其派生类保留一个函数的名字,以便派生类根据需要对它进行定义。如果在基类中没有保留函数名字,则无法实现多态性。
- 如果在一个类中声明了纯虚函数,而在其派生类中没有对该函数定义,则该虚函数在派生类中仍然为纯虚函数。
抽象类
- 如果声明了一个类,一般可以用它定义对象。但是在面向对象程序设计中,往往有一些类,它们不用来生成对象。定义这些类的唯一目的是用它作为基类去建立派生类。它们作为一种基本类型提供给用户,用户在这个基础上根据自己的需要定义出功能各异的派生类。用这些派生类去建立对象。
- 这种不用来定义对象而只作为一种基本类型用作继承的类,称为抽象类(abstract class),由于它常用作基类,通常称为抽象基类(abstract base class)。凡是包含纯虚函数的类都是抽象类。因为纯虚函数是不能被调用的,包含纯虚函数的类是无法建立对象的。抽象类的作用是作为一个类族的共同基类,或者说,为一个类族提供一个公共接口。
- 如果在抽象类所派生出的新类中对基类的所有纯虚函数进行了定义,那么这些函数就被赋予了功能,可以被调用。这个派生类就不是抽象类,而是可以用来定义对象的具体类(concrete class)。如果在派生类中没有对所有纯虚函数进行定义,则此派生类仍然是抽象类,不能用来定义对象。
- 虽然抽象类不能定义对象(或者说抽象类不能实例化),但是可以定义指向抽象类数据的指针变量。当派生类成为具体类之后,就可以用这种指针指向派生类对象,然后通过该指针调用虚函数,实现多态性的操作。
例:虚函数和抽象基类的应用
#include <iostream>
using namespace std;
//声明抽象基类Shape
class Shape
{
public:
virtual float area() const { return 0.0; } //虚函数
virtual float volume() const { return 0.0; } //虚函数
virtual void shapeName() const = 0; //纯虚函数 就是声明虚函数时被初始化为0的函数
};
//声明Point类
class Point :public Shape //Point是Shape的公用派生类
{
public:
Point(float = 0, float = 0); //声明构造函数
void setPoint(float, float); //设置坐标值
float getX() const { return x; } //读x坐标,getX函数为常成员函数
float getY() const { return y; } //读y坐标,getY函数为常成员函数
virtual void shapeName() const { cout << "Point:"; } //对虚函数进行再定义
friend ostream& operator<<(ostream&, const Point&); //友元重载运算符<<
protected: //受保护成员
float x, y;
};
//定义Point类成员函数
Point::Point(float a, float b) { x = a;y = b;} //定义构造函数,对x,y初始化
void Point::setPoint(float a, float b) { x = a; y = b; } //设置x,y的坐标值,对x,y赋予新值
ostream& operator<<(ostream& output, const Point& p) //重载运算符"<<",使之能输出点的坐标
{
output << "[" << p.x << "," << p.y << "]";
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; //读取半径值
virtual float area() const; //对虚函数进行再定义
virtual void shapeName() const { cout << "Circle:"; } //对虚函数进行再定义
friend ostream& operator<<(ostream&, const Circle&); //重载运算符"<<"
protected:
float radius;
};
//声明Circle类成员函数
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 << "[" << c.x << "," << c.y << "],r=" << c.radius;
return output;
}
class Cylinder :public Circle
{
public:
Cylinder(float x = 0, float y = 0, float r = 0, float h = 0); //构造函数
void setHeight(float); //设置圆柱高
virtual float area() const; //重载虚函数
virtual float volume() const; //重载虚函数
virtual void shapeName() const { cout << "Cylinder:"; } //重载虚函数
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::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 << "[" << cy.x << "," << cy.y << "],r=" << cy.radius << ",h=" << cy.height;
return output;
}
int main() {
Point point(3.2, 4.5); //建立Point类对象point
Circle circle(2.4, 1.2, 5.6); //建立Cirlce类对象circle
Cylinder cylinder(3.5, 6.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(); //用指针建立动态关联
cout << "x=" << point.getX() << ",y=" << point.getY() << "\narea=" << pt->area()
<< "\nvolume=" << pt->volume() << "\n\n"; //输出点的数据
pt = &circle; //指针指向Circle类对象
pt->shapeName(); //动态关联
cout << "x=" << circle.getX() << ",y=" << circle.getY() << "\narea=" << pt->area()
<< "\nvolume=" << pt->volume() << "\n\n"; //输出圆的数据
pt = &cylinder; //指针指向Cylinder类对象
pt->shapeName(); //动态关联
cout << "x=" << cylinder.getX() << ",y=" << cylinder.getY() << "\narea=" << pt->area()
<< "\nvolume=" << pt->volume() << "\n\n"; //输出圆柱的数据
return 0;
}
程序分析:
Shape类:
- Shape类有3个成员函数,没有数据成员。3个成员函数都声明为虚函数,其中shapeName声明为纯虚函数,因此Shape是一个抽象基类。shapeName函数的作用是输出具体的形状(如点、圆、圆柱体)的名字,这个信息是与相应的派生类密切相关的,显然这不应当在基类中定义,而应在派生类中定义。所以把它声明为纯虚函数。Shape虽然是抽象基类,但是也可以包括某些成员的定义部分。类中两个函数area(面积)和volume(体积)包括函数体,使其返回值为0(因为可以认为点的面积和体积都为0)。由于考虑到在Point类中不再对area和volume函数重新定义,因此没有把area和volume函数也声明为纯虚函数。在Point类中继承了Shape类的area和volume函数。这3个函数在各派生类中都要用到。
Point类:
- Point从Shape继承了3个成员函数,由于点是没有面积和体积的,因此不必重新定义area和volume。虽然在Point类中用不到这两个函数,但是Point类仍然从Shape类继承了这两个函数,以便其派生类继承它们。shapeName函数在Shape类中是纯虚函数, 在Point类中要进行定义。Point类还有自己的成员函数(setPoint,getX, getY)和数据成员(x和y)。
Circle类:
- 在Circle类中要重新定义area函数,因为需要指定求圆面积的公式。由于圆没有体积,因此不必重新定义volume函数,而是从Point类继承volume函数。shapeName函数是虚函数,需要重新定义,赋予新的内容(如果不重新定义,就会继承Point类中的shapeName函数)。此外,Circle类还有自己新增加的成员函数(setRadius,getRadius)和数据成员(radius)。
Cylinder类:
- Cylinder类是从Circle类派生的。由于圆柱体有表面积和体积,所以要对area和 volume函数重新定义。虚函数shapeName也需要重新定义。此外,Cylinder类还有自已 的成员函数setHeight和数据成员radius。
主函数:
- 主函数中先后用静态关联和动态关联的方法输出结果。通过对象名point,circle和cylinder调用shapeName函数,属于静态关联。通过定义一个指向基类Shape对象的指针变量pt,使它先后指向3个派生类对象,然后通过指针调用各函数,这是动态关联。
运行结果: