1.虚函数的作用
C++中的虚函数是用于解决动态多态性的问题。所谓虚函数(virtual function),就是在基类声明函数是虚拟的,并不是实际存在的函数,然后在派生类中才正式定义此函数。
那么虚函数有何作用呢?我们先来看看这样一段程序:在Circle类中:
void Circle::display(){cout<<"Center=["<
{cout<<"Center=["<
#include
#include"Point.h"
#include"Circle.h"
#include"Cylinder.h"
using namespace std;
int main()
{
Circle a(1.1,1.1,1.1);
Cylinder b(2.2,2.2,2.2,2);
Circle *pt=&a;
pt->display();
pt=&b;
pt->display();
return 0;
}
得出结果如下:
可以从结果中看出,结果中并没有把Cylinder类的对象b的全部数据输出(缺少了height),这是因为当使pt指向对象b时,再调用pt->display()时,并没有如想象中那样调用了b中的display函数,而是调用了a中的display函数,这就是同名覆盖原则下编译的效果。(在同一个类中是不能定义两个名字相同、参数个数和类型都相同的函数,否则就是“重复定义”。但由于有类的继承,所以这些“完全同名函数”可以在不同的类里出现。编译时,编译系统会按照同名覆盖的原则决定调用的对象。)
[相关说明:基类指针(pt)是用来指向基类对象的,如果用它指向派生类对象,则自动进行指针类型转换,将派生类的对象的指针先转换为基类的指针,这样,基类指针指向的是派生类对象中的基类部分。所以,如不修改程序,是无法通过基类指针去调用派生类对象中的成员函数的。]
如果想调用对象b中的display函数,可以新定义一个指向Cylinder类对象的指针变量,再使该新的指针变量指向Cylinder类的对象。但是,如果一个基类派生出多个基类,每个派生类又派生出多个派生类,形成了同一基类的类族,而每个派生类都有同名函数,要想在程序中调用同一类族的不同类的同名函数,就要定义大量的指向各派生类的指针变量,这会很麻烦。而虚函数的应用可以很好地解决这一问题。
现在让我们在Circle类中的display函数的定义前加上关键字virtual,如下:virtual void display();
再编译一下测试程序,得出结果如下:
由程序运行结果我们可以看出:用同一种调用方式,用同一个指针变量(pt是指向基类对象的指针变量),可以调用同一类族中不同类的虚函数,这就是虚函数实现的动态多态性的体现:同一类族中不同类的对象,对同一函数调用作出不同的响应。
总结:虚函数的作用:允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。
(注:在基类中定义的非虚函数会在派生类中被重新定义,如果用基类指针调用该成员函数,则系统会调用对象中基类部分的成员函数;如果用派生类指针调用该成员函数,则系统会调用派生类对象中的成员函数,而这并不是多态性行为的。)
在一个类里,函数重载处理的是同一层次上的同名函数问题;而虚函数处理是不同层次(多个类)上的同名函数问题。前者是横向,后者是纵向。同一类族的虚函数的首部是相同的,而函数重载时函数的首部是不同的(参数个数或类型不同)。
3.声明虚函数的注意事项
(1)只能用virtual声明类的成员函数,把它作为虚函数,而不能将类外的普通函数声明为虚函数。
(2)一个成员函数被声明为虚函数后,在同一类族中的类就不能再定义一个非virtual的但与该虚函数具有相同的参数(包括个数和类型)和函数返回值类型的同名函数。
4.相关概念
关联(blinding):确定调用的具体对象的过程。
函数重载和通过对象名调用的虚函数,在编译时即可确定其调用的虚函数属于一类,其过程称为静态关联(static binding),又称早期关联(early binding)。
而另一种通过基类指针调用虚函数,由于并没有指定对象名,而编译时只能作静态的语法检查,则只从词句形式上是无法确定调用对象的。在这种情况下,编译系统把它放到运行阶段处理,在运行阶段确定关联关系。在运行阶段,基类指针变量先指向了某一个类对象,然后通过此指针变量调用该对象中函数。此时,调用哪一个对象的函数无疑是很确定的。这种过程称为动态关联(dynamic binding),也称滞后关联(late binding)。
使用虚函数时,系统要有一定的空间开销。当一个类带有虚函数时,编译系统会为该类构造一个虚函数表(virtual function table,简称vtable),它是一个指针数组,存放每个虚函数的入口地址。系统在进行动态关联时的时间开销是很少的,因此,虚函数多态性是高效的。
5.虚析构函数
下面下来看一个例子:
我们先定义一个Father类:
#ifndef FATHER
#define FATHER
#include
using namespace std;
class Father
{public:
Father(){}
virtual ~Father(){cout<<"I have deleted Father!"<
然后再定义一个派生类Kid类:
#ifndef KID
#define KID
#include
using namespace std;
class Kid:public Father
{public:
Kid(){}
~Kid(){cout<<"I have deleted Kid!"<
下面是测试程序:
#include
#include"Father.h"
#include"Kid.h"
using namespace std;
void main()
{
Father *p=new Kid;
delete p;
}
测试结果如下图:
在测试结果中我们可以发现,系统只执行基类的析构函数,而不执行派生类的析构函数。原因是前文我们所说的同名覆盖原则之下的编译。
所以我们要把基类的析构函数定义为虚函数,以使编译系统在撤销对象时,不会发生没有执行派生类的析构函数。
在Fateher类的析构函数前加上关键字virtual,如下:
virtual ~Father(){cout<<"I have deleted Father!"<
则测试程序的结果如下:
把基类中的析构函数定义为虚函数后,则由其派生的所有派生类的析构函数都自动成为虚函数,即便基类的析构函数与派生类的析构函数不同名。
[注:构造函数不能声明为虚函数。这是因为在执行制造函数时类对象还未完成建立过程,就谈不上把函数与类对象的绑定。]
参考资料:《C++程序设计(第2版)》.谭浩强.清华大学出版社.2011.8.pag398-405