虚函数表是实现多态的核心,所谓多态,就是“一个函数,多种实现”,当我们通过类指针或引用调用一个函数接口时,编译器在运行期间将会根据该指针或引用实际指向的对象来调用函数,而这就是通过虚函数表来实现的,换种方式说,虚函数表,完成了动态联编中寻找虚函数哪个执行代码块的任务。
现在来理一下编译器使用虚函数表的过程:
编译器在处理虚函数时,会为基类及相关派生类添加一个隐藏成员,这个成员是用来指向虚函数表的指针,对于每个对象而言,该指针是不同的,该指针指向的虚函数表也是不同的,如果我们把这个指针视作vpr,也就是说:每个类对象具有不同的vpr和虚函数表。
对于基类而言,其虚函数表指向其所有虚函数的地址表,这个地址表就是指向代码块的所在地址。
对于派生类而言,情况则相对复杂,因为它继承了基类的虚函数。
如果派生类没有重新定义其继承的虚函数,那么这个虚函数表将保存其基类的函数地址;如果它重新定义了其继承的虚函数,则将保存它的新的函数地址,而将其基类中对应的函数实现给覆盖掉。
如果派生类自己新定义了一些虚函数,那么这些虚函数地址也将保存到这个表中。
我们可以结合下面这个示意图示意图来理解:
假定Derive继承于Base,图中所示的函数都是虚函数。
由于派生类重新实现了基类中的 f 方法,那么其虚函数表如下:
那么一旦运行,这些虚函数表是如何工作的呢?
如果我们通过引用或指针调用了虚函数,那么根据这些指针或引用所指向的实际对象,编译器会找到实际对象的隐藏对象vpr。
然后,根据vpr,我们就可以找到对象的虚函数表,然后根据虚函数表,我们就可以找到所要执行的代码块的地址。
由上可见,虚函数表其实会增加我们存储地址的空间;
那么针对哪些地方我们要使用虚函数,哪些地方又不能使用呢?
要使用的地方:
- 析构函数,这个前面已经说了,如果delete一个基类指针,如果析构函数未声明为虚函数,那么即使该引用或指针指向的是个派生类,也将只调用基类析构函数,而无法执行析构函数,所以,为安全起见,基类析构函数必须为虚函数;
- 需要在派生类中重新定义的函数,这是当然的;
不能使用的地方:
- 构造函数,我们应该不会有想要在派生类中重新定义基类构造函数的冲动,这样有什么用?