- 虚函数的机制
当类中定义有虚函数时,编译器会将该类中所有虚函数的首地址保存在一张地址表中,这张表被称为虚函数地址表。编译器还会在类中添加一个虚表指针。
举例:
CVirtual类的构造函数中没有进行任何操作,但是我们来看构造函数内部,还是有一个赋初值的操作:
这个地址指向的是一个数组:
这些数组中的内容就是虚函数的指针:
值得注意的是,如果没有虚指针的存在,那么CVirtual大小就是4字节。有了这个指针存在就是8字节。
本例子中,使用了一个空的构造函数,但是编译器自己擅自插入了代码,实现了对虚表的初始化。如果我们没有提供任何构造函数的话,那么编译器就会提供一个默认的构造函数对虚表进行初始化。
当函数被调用时,会间接访问虚表,得到对应的虚函数的地址,并调用执行。这种通过虚表间接寻址访问的情况只有在使用对象的指针或引用来调用虚函数的时候才会出现。当直接使用对象调用自身的虚函数时,没有必要查表访问。因为已经明确调用的是自身的成员函数了,根本没有构成多态性。
举例:
直接通过对象调用虚函数的时候,就是直接用对象的地址作为隐含参数传递给这个虚函数:
这个虚函数此时和普通的成员函数没有区别。之所以要隐含传递对象的地址,是为了能够准确适用对象中所包含的数据成员。
但是如果构成了多态,调用方式就不同了:
因为你实际上不知道pcv指针指向的具体类型是什么,所以要到虚表中找到所指向的真正的对象的那个虚函数。
虚表指针的初始化,是判断一个函数是构造函数的充分条件。
析构函数对虚表如何操作?在考虑这个问题之前,我们先要知道,为什么析构函数要使用虚函数:
如果父类和子类的虚函数分别如下所示:
我们在执行delete指针之后,会是如下流程:
即只调用了父类的虚函数。而如果把析构函数设置为虚的:
则是如下调用流程:
delete删除指针的时候调用的是子类的虚函数,而子类的虚函数内部又调用了父类的虚函数。而调用父类的虚函数之前,ecx指针中仍保留的是子类对象的首地址:
子类的析构函数调用自身的虚成员函数:
随后调用父类的析构函数:
父类的析构函数中没有间接寻址,直接调用了Show1和Show2:
但是无论是子类还是父类的虚析构函数中都会有这么一步操作:把当前类的虚表的首地址赋值到虚表指针当中去。这是为了防止在析构函数中调用虚函数时取到非自身的虚表。为什么要这么做?举例说明:
先调用A的构造函数:
A类填充虚表:
调用虚函数:
调用完A的构造函数,继续往下执行B的构造函数中的其余部分,为了能够正常调用B的func2,这里必须要还原虚表:
析构函数中同理。
- 虚函数的识别