当类中定义有虚函数时,编译器会将该类中全部虚函数的首地址保存在一张地址表中,这张表被称为虚函数地址表。编译器还会在类中添加一个虚表指针。html
举例:数组
CVirtual类的构造函数中没有进行任何操做,可是咱们来看构造函数内部,仍是有一个赋初值的操做:函数
这个地址指向的是一个数组:this
这些数组中的内容就是虚函数的指针:spa
值得注意的是,若是没有虚指针的存在,那么CVirtual大小就是4字节。有了这个指针存在就是8字节。.net
本例子中,使用了一个空的构造函数,可是编译器本身擅自插入了代码,实现了对虚表的初始化。若是咱们没有提供任何构造函数的话,那么编译器就会提供一个默认的构造函数对虚表进行初始化。指针
当函数被调用时,会间接访问虚表,获得对应的虚函数的地址,并调用执行。这种经过虚表间接寻址访问的状况只有在使用对象的指针或引用来调用虚函数的时候才会出现。当直接使用对象调用自身的虚函数时,没有必要查表访问。由于已经明确调用的是自身的成员函数了,根本没有构成多态性。htm
举例:对象
直接经过对象调用虚函数的时候,就是直接用对象的地址做为隐含参数传递给这个虚函数:blog
这个虚函数此时和普通的成员函数没有区别。之因此要隐含传递对象的地址,是为了可以准确适用对象中所包含的数据成员。
可是若是构成了多态,调用方式就不一样了:
由于你实际上不知道pcv指针指向的具体类型是什么,因此要到虚表中找到所指向的真正的对象的那个虚函数。
虚表指针的初始化,是判断一个函数是构造函数的充分条件。
析构函数对虚表如何操做?在考虑这个问题以前,咱们先要知道,为何析构函数要使用虚函数:
若是父类和子类的虚函数分别以下所示:
咱们在执行delete指针以后,会是以下流程:
即只调用了父类的虚函数。而若是把析构函数设置为虚的:
则是以下调用流程:
delete删除指针的时候调用的是子类的虚函数,而子类的虚函数内部又调用了父类的虚函数。而调用父类的虚函数以前,ecx指针中仍保留的是子类对象的首地址:
子类的析构函数调用自身的虚成员函数:
随后调用父类的析构函数:
父类的析构函数中没有间接寻址,直接调用了Show1和Show2:
可是不管是子类仍是父类的虚析构函数中都会有这么一步操做:把当前类的虚表的首地址赋值到虚表指针当中去。这是为了防止在析构函数中调用虚函数时取到非自身的虚表。为何要这么作?举例说明:
先调用A的构造函数:
A类填充虚表:
调用虚函数:
调用完A的构造函数,继续往下执行B的构造函数中的其他部分,为了可以正常调用B的func2,这里必需要还原虚表:
析构函数中同理。
1)特征:
一、类中隐式定义了一个数据成员;
二、该数据成员在首地址处;
三、构造函数会将此数据成员初始化为某个数组的首地址;
四、这个地址属于数据区,是相对固定的地址;
五、数组内每一个元素都是函数的指针;
六、数组中的这些函数被调用时,第一个参数必然是this指针;
七、这些函数内部有可能对this指针进行相对间接的访问。
2)验证父类和子类的虚表指针:
举例:
初始化父类以后:
父类的两个虚函数地址为:
调用完父类构造函数以后会从新赋值一个虚表:
咱们发现,A的虚表中和B的虚表中的第一个函数地址是相同的,不一样的是第二个函数的地址。在构造B的时候先构造A,而在构造A的时候要赋值一个虚表指针,是为了防止在A的构造函数中使用使用了虚函数,而无心间调用了B的虚函数。而实际上,构造完B以后,B中就不存在刚刚A的那个虚表指针了。
借助OD找到A虚表的地址和B虚表的地址:
因而,先根据交叉引用找到了A的构造函数:
再借助交叉引用找到B的构造函数:
固然若是B中有多个构造函数,和一个析构函数时是什么状况呢?会有两个构造函数引用它:
举例:
全局对象的构造函数调用以后会调用一个函数来登记析构函数的地址:
此时push进去的参数是一个函数的地址41A4D0:
进入call 4110F5,这里边的call 4158A8就是等同于atexit的做用:
里边会把这个函数的地址传递给onexit:
参考:
http://bbs.csdn.net/topics/360161935
https://www.2cto.com/kf/201408/326530.html
参考这两个连接了解到,传递给onexit的函数,总会在main函数执行完毕以后执行。因此能够推断出41A4D0是个析构函数。
看下41A4D0中的内容:
进入被标记的那个call,发现这就是一个析构函数,而且有一个恢复虚表指针的操做:
交叉引用看一下哪些地方用到了这个虚表:
发现只有两个交叉引用,左边的那个是析构函数,右边的那个是构造函数。虽然这个全局类包括了三种形式的构造函数,可是咱们的程序中只用了一种形式,因此只有其中一种形式的构造函数对虚表进行了操做: