下面是一段关于虚函数的程序:
#include <iostream>
using namespace std;
class Base {
public:
void funA() { cout << "in Base::funA\n"; }
void funB() { cout << "in Base::funB\n"; }
virtual void funC() { cout << "in Base::funC\n"; }
virtual void funD() { cout << "in Base::funD\n"; }
virtual ~Base() {}
};
class Derived : public Base {
public:
void funA(int n) { cout << "in Derived::funA\n"; } //Base::funA被隐藏
void funB() { cout << "in Derived::funB\n"; } //Base::funB被隐藏
virtual void funC(int n) { cout << "in Derived::funC\n"; } //Base::funC被隐藏
virtual void funD() { cout << "in Derived::funD\n"; } //Base::funD被覆盖
};
int main()
{
Derived derived;
derived.funA(1); //in Derived::funA
derived.funB(); //in Derived::funB
derived.funC(2); //in Derived::funC
derived.funD(); //in Derived::funD
//derived.funA(); //无法调用
//derived.funC(); //无法调用
Base *pBase = &derived;
pBase->funA(); //in Base::funA
pBase->funB(); //in Base::funB
pBase->funC(); //in Base::funC
pBase->funD(); //in Derived::funD(多态)
//pBase->funA(1); //无法调用
//pBase->funC(2); //无法调用
return 0;
}
根据上面的程序,我们先讨论虚函数的一些要点。
在基类方法的声明中使用关键字virtual可使该方法在基类以及所有的派生类(包括从派生类派生出来的类)中是虚的。
如果使用指向对象的引用或指针来调用虚方法,程序将使用为对象类型定义的方法,而不使用为引用或指针类型定义的方法。这称为动态联编。这种行为非常重要,因为这样基类指针或引用可以指向派生类。例如funD,由Base类型的指针pBase去调用时,执行的是pBase指向的derived对象的funD方法,这便是多态的一种体现。
如果定义的类被用作基类,则应将那些要在派生类中重新定义的类方法声明为虚的。
静态联编与动态联编
将源代码中的函数调用解释为执行特定的函数代码块被为函数名联编。在编译过程中进行联编被称为静态联编。另一方面,由于虚函数的特性,编译器必须生成能够在程序进行时选择正确的虚方法的代码,这被称为动态联编。
由于静态联编的效率较高,因些,如果要在派生类中重新定义基类的方法,则将它设置为虚方法;否则设置为非虚方法。
重载与覆盖
成员函数被重载的特征:
(1)相同的范围(在同一个类中) ;
(2)函数名字相同;
(3)参数不同;
(4)virtual 关键字可有可无。
覆盖是指派生类函数覆盖基类函数(例如funD函数),特征是:
(1)不同的范围(分别位于派生类与基类) ;
(2)函数名字相同;
(3)参数相同;
(4)基类函数必须有 virtual 关键字。
隐藏的定义
这里“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,规则如下:(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。例如funA, funC。
(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。例如funB。
提示:如果基类声明被重载了,则应在派生类中重新定义所有的基类版本,如果只定义一个版本,则其它版本将被隐藏。
构造函数不能是虚函数
(1) 从存储空间角度,虚函数对应一个指向vtable虚函数表的指针,这大家都知道,可是这个指向vtable的指针其实是存储在对象的内存空间的。问题出来了,如果构造函数是虚的,就需要通过 vtable来调用,可是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?所以构造函数不能是虚函数。
(2) 从使用角度,虚函数主要用于在信息不全的情况下,能使重载的函数得到对应的调用。构造函数本身就是要初始化实例,那使用虚函数也没有实际意义呀。所以构造函数没有必要是虚函数。虚函数的作用在于通过父类的指针或者引用来调用它的时候能够变成调用子类的那个成员函数。而构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。
(3) 构造函数不需要是虚函数,也不允许是虚函数,因为创建一个对象时我们总是要明确指定对象的类型,尽管我们可能通过实验室的基类的指针或引用去访问它但析构却不一定,我们往往通过基类的指针来销毁对象。这时候如果析构函数不是虚函数,就不能正确识别对象类型从而不能正确调用析构函数。
(4) 从实现上看,vbtl在构造函数调用后才建立,因而构造函数不可能成为虚函数从实际含义上看,在调用构造函数时还不能确定对象的真实类型(因为子类会调父类的构造函数);而且构造函数的作用是提供初始化,在对象生命期只执行一次,不是对象的动态行为,也没有必要成为虚函数。
(5) 当一个构造函数被调用时,它做的首要的事情之一是初始化它的VPTR。因此,它只能知道它是“当前”类的,而完全忽视这个对象后面是否还有继承者。当编译器为这个构造函数产生代码时,它是为这个类的构造函数产生代码——既不是为基类,也不是为它的派生类(因为类不知道谁继承它)。所以它使用的VPTR必须是对于这个类的VTABLE。而且,只要它是最后的构造函数调用,那么在这个对象的生命期内,VPTR将保持被初始化为指向这个VTABLE, 但如果接着还有一个更晚派生的构造函数被调用,这个构造函数又将设置VPTR指向它的 VTABLE,等.直到最后的构造函数结束。VPTR的状态是由被最后调用的构造函数确定的。这就是为什么构造函数调用是从基类到更加派生类顺序的另一个理由。但是,当这一系列构造函数调用正发生时,每个构造函数都已经设置VPTR指向它自己的VTABLE。如果函数调用使用虚机制,它将只产生通过它自己的VTABLE的调用,而不是最后的VTABLE(所有构造函数被调用后才会有最后的VTABLE)。
析构函数应当是虚函数,除非类不用做基类
如果析构函数不是虚的,则将只调用对应于指针类型的析构函数。举例说明:
子类B继承自基类A;A *p = new B; delete p;
1) 此时,如果类A的析构函数不是虚函数,那么delete p;将会仅仅调用A的析构函数,只释放了B对象中的A部分,而派生出的新的部分未释放掉。
2) 如果类A的析构函数是虚函数,delete p; 将会先调用B的析构函数,再调用A的析构函数,释放B对象的所有空间。