虚函数是面向对象编程中函数的一种特定形态,是C++中用于实现多态的一种有效机制
指向基类的指针在操作它的多态对象时,会根据不同的类对象调用相应的对象函数,这个函数就是虚函数,虚函数用virtual修饰函数名。虚函数的作用是在程序的运行阶段动态地选择合适的成员函数,定义了虚函数后,可以在派生类中对虚函数重新定义。在派生类中重新定义的函数应与基类的虚函数具有相同的形参个数和形参类型(参数类型顺序也要一致),以实现统一的接口。如果在派生类中没有对虚函数重新定义,则它继承基类的虚函数
使用虚函数需要注意以下几点:
只需要在声明函数的类体中使用virtual将函数声明为虚函数,而定义函数时不需要使用关键字virtual
当将基类中的某一成员函数声明为虚函数后,派生类中的同名函数自动成为虚函数
如果声明了某个成员函数为虚函数,则在该类中不能出现与这个成员函数同名并且返回值、参数个数、类型都相同的非虚函数
非类的成员不能定义为虚函数,全局函数以及类类的成员函数中静态成员函数和构造函数也不能定义为虚函数,但是析构函数可以定义为虚函数
基类的析构函数应该定义为虚函数,这样在实现多态的时候不会造成内存泄漏。如果基类析构函数未声明为virtual,当使用基类指针指向派生类时,delete指针只会调用基类的析构函数,而不会调用派生类的析构函数。如果声明为virtual,则会先调用派生类的析构函数,再调用基类的析构函数,这样就能够避免内存泄漏
指针声明不调用构造函数,只有使用new创建对象的时候,才会调用构造函数
虚函数是通过一张虚函数表(Virtual Table)来实现的。该表是一个类的虚函数的地址表,表中保存了虚函数的地址。这样,在由虚函数的实例中,此表被分配在实例的内存中,所以当用父类的指针来操作一个子类的时候,这种虚函数表指明了实际应该调用的函数
C++的编译器能够保证虚函数表的指针存在于对象实例中最前面的位置,通过对象实例的地址得到这张虚函数表,然后就可以遍历其中的函数指针,并调用相应的函数。例如:
class Base
{
public:
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
virtual void h() { cout << "Base::h" << endl; }
};
int main()
{
typedef void(*Fun)(void);
Base b;
Fun pFun = NULL;
cout << "虚函数地址:" << (int*)(&b) << endl;
cout << "虚函数表-第一个函数地址:" << (int*)*(int*)(&b) << endl;
pFun = (Fun)*((int*)*(int*)(&b));
pFun();
}
程序的运行结果为:
虚函数地址:006FF7AC
虚函数表-第一个函数地址:007C3C34
Base::f
通过这个示例可以看到,可以强行把&b转成int*
,取得虚函数表的地址,然后再次取值就可以得到第一个虚函数的地址了,也就是Base::f()
,这在上面的程序中得到了验证(把int*
强制转成函数指针)。调用Base::g()
和Base::h()
的代码如下:
(Fun)*((int*)*(int*)(&b)+0); //Base::f()
(Fun)*((int*)*(int*)(&b)+1); //Base::g()
(Fun)*((int*)*(int*)(&b)+2); //Base::h()
编译器发现一个类中有虚函数,便会立即为此类生成虚函数表,虚函数表的各表项为指向对应虚函数的指针。编译还会在此类中隐含插入一个指针vptr(对VC编译器来说,它插在类的第一个位置上)指向虚函数表。当调用此类的构造函数时,编译器会隐含执行vptr于vtable的关联代码,将vptr指向对应的vtable关联起来。另外,在调用类的构造函数时,指向基础类的指针此时已经变成指向具体的类的this指针,这样依靠此this指针即可得到正确的vtable。这样才能真正与函数体进行连接,这就是动态联编,实现多态的基本原理
虽然虚函数很有效,但是不可以把每个函数都声明为虚函数。因为使用虚函数是要付出代价的。由于每个虚函数的对象在内存中都必须维护一个虚函数表,因此在使用虚函数时,尽管带来了方便,却会额外产生一个系统开销。如果仅是一个很小的类,且不想派生其他类,那么没必要使用虚函数
C++中通过虚函数实现多态。虚函数的本质就是通过基类指针访问派生类定义的函数。每个含有虚函数的类,其实例对象内部都有一个虚函数表指针。该虚函数表指针初始化为本类的虚函数表的内存地址。所以,在程序中,不管对象类型如何转换,该对象内部的虚函数表指针都是固定的,这样才能实现动态地对对象函数进行调用
需要注意的是:
只有通过指针或者引用调用虚函数才能达到多态的效果,如果通过对象直接调用,就没有多态的效果
在基类的构造和析构函数中调用虚函数,没有多态性。因为在构造一个子类的对象时,会先调用基类的构造函数,此时子类没有完成构造,还没有初始化,如果此时在基类的构造函数中调用虚函数,如果可以,就是调用一个还没有被初始化的对象,这是很危险的行为,有些编译器会对此进行如下设计:在这种情况下默认调用父类的函数(具体的实现与编译器有关)。所以,C++中最好不在父类的构造函数中调用虚函数
纯虚函数是一种特殊的虚函数,格式一般如下:
class <类名>
{
virtual 函数返回值类型 虚函数名(形参表) = 0;
...
};
由于在很多情况下,基类中不能对虚函数给出有意义的实现,只能把函数的实现留给派生类。如果基类中有虚函数,那么在子类中必须实现这个纯虚函数,否则子类将无法被实例化,也无法实现多态
含有纯虚函数的类称为抽象类,抽象类不能生成对象。纯虚函数永远不会被调用,主要用来统一管理子类对象
只有类的成员函数才能被声明为虚函数
静态成员函数不能为虚函数,因为静态成员函数属于类,而不属于对象,但调用虚函数需要从一个实例中指向虚函数表的指针,以得到函数的地址,因此调用虚函数需要一个实例化的对象,二者相互矛盾
内联函数不能为虚函数
构造函数不能为虚函数
析构函数可以为虚函数,而且通常声明为虚函数
构造函数不能是虚函数,因为构造函数是在对象完全构造之前运行的。运行的构造函数前,对象还没有生成,更谈不上动态类型了。构造函数是初始化虚函数表指针,而虚函数放到虚函数表里,当要调用虚函数的时候首先要知道虚函数表指针,这就存在矛盾了。