1.单继承中的虚函数表
整体代码
#include<iostream> using namespace std; class Base { public: virtual void Func1() { cout << "Base::Func1()" << endl; } virtual void Func2() { cout << "Base::Func2()" << endl; } void Func3() { cout << "Base::Func3()" << endl; } private: int _b = 1; }; class Derive : public Base { public: virtual void Func1() { cout << "Derive::Func1()" << endl; } virtual void Func4() { cout << "Base::Func4()" << endl; } private: int _d = 2; }; typedef void(*VF_PTR)(); // typedef void(*)() VF_PTR; void PrintVFTable(VF_PTR table[])//函数指针数组 { int i = 0; for (i = 0; table[i] != nullptr; i++) { printf("[%d]:%p\n",i, table[i]); VF_PTR f = table[i]; f(); } } int main() { Base b; Derive d; PrintVFTable((VF_PTR*)*(int*)&b); cout << endl; PrintVFTable((VF_PTR*)*(int*)&d); return 0; }
在子类中实现一个虚函数Func4,但是不构成重写
Func4函数并没有进入虚表中
通过查询内存发现,虚表指针中存在三个地址,而其中两个正好为监视中的两个地址
猜测 0x00c4146a 就是Func4的地址
用程序打印虚表
虚表本质是一个函数指针数组
VS中在数组最后放了一个nullptr,这样就可以解决在不同虚表中的个数不同的问题
typedef一个函数指针 为VF_PTR
正常来说 要写成将VF_PTR放在后面
但是由于函数指针的特殊性,定义变量都要放在中间
如何寻找到虚表地址
想要虚表的地址,就可以通过找到虚表的指针
而这个虚表指针在对象中,这个指针在对象的前4个(32位)或者8个字节(64位)上面
以32位为例,如何取对象的前4个字节
强制转换为int*
* (int* )&b
首先取到Base* ,将其强制转换为int*,代表前四个字节的地址,再解引用是int类型,把前四个字节取出来
但是由于PrintVFTable函数参数是 函数指针数组
(VF_PTR*) * (int *)&b
如果这个数组是int类型,就需要 一个int * 指针去指向
同理 ,该数组为 VF_PTR类型,需要一个VF_PTR *指针去指向
所以需将 int 类型 再次强制转换为 VF_PTR * ,使其指向这个数组
缺陷
但是这种写法具有一定的局限性,只能在32位跑,因为32位指针大小为4个字节
而64位下就不行了,64位下指针大小为8个字节
运行程序打印虚表,确实了解到多了一个地址
把虚表的地址拿出来赋给函数指针,用函数指针去调用函数
这里发现 监视中没有出现的地址确实是Func4函数的地址
虚表存在哪里?
由于常量区地址与虚表的地址最为接近,所以说明虚表在常量区/代码段上
2.多继承中的虚函数表
整体代码
class Base1 { public: virtual void func1() { cout << "Base1::func1" << endl; } virtual void func2() { cout << "Base1::func2" << endl; } private: int b1; }; class Base2 { public: virtual void func1() { cout << "Base2::func1" << endl; } virtual void func2() { cout << "Base2::func2" << endl; } private: int b2; }; class Derive : public Base1, public Base2 { public: virtual void func1() { cout << "Derive::func1" << endl; } virtual void func3() { cout << "Derive::func3" << endl; } private: int d1; }; typedef void(*VF_PTR)(); // typedef void(*)() VF_PTR; void PrintVFTable(VF_PTR table[])//函数指针数组 { int i = 0; for (i = 0; table[i] != nullptr; i++) { printf("[%d]:%p->", i, table[i]); VF_PTR f = table[i]; f(); } } int main() { Derive d; PrintVFTable( (VF_PTR*) *(int*) &d); cout << endl; /*PrintVFTable((VF_PTR*)*(int*)( (char*)&d+ sizeof(Base1) ) );*/ Base2* ptr2 = &d; PrintVFTable((VF_PTR*)*(int*)(ptr2)); return 0; }
寻找虚表地址
Derive 作为Base1 和Base2的子类,所以Derive内部有两张虚表
正常来说,Derive内部还存在一个func3函数,这个函数放在哪里了呢?
借助打印虚表来查看,这里的打印虚表依旧可以使用单继承中的那个
base1的虚表指针 正好在对象的前4个字节处,直接可以使用求出虚表指针 去指向base1的虚表
方法1 : base2的虚表指针 需要加上base1的大小
但是这里要注意一个问题,若写成 PrintVFTable((VF_PTR*)(int)( &d+ sizeof(Base1) ) )
写的并不对,d本身是一个Derive类型,&d后变为Derive* 的一个指针,+1 跳转的是Derive类型的字节大小
而该设计想要每次+1跳转1个字节,所以需要强制转换char*
方法2 :切片自动偏移
两种方法的结果都是一样的
注意事项
多继承派生类增加的的虚函数在第一个虚表中
多继承重写后的func1地址为什么不同?
ptr1调用函数——一次jmp
找到 Base1虚表里的地址 0x00e21230 ,再call这个地址
只需要jmp一次 就可以找到实际真正执行的函数地址ptr1调用地址属于正常调用
ptr2 调用函数——多次jmp
ptr2调用地址,需要 多次jmp 才能找到真正的函数地址
ecx存的是this指针
ecx,8 目的是修正this指针的位置
最终Base1和Base2都是执行同一个函数的指令
- ptr1->func1 调用的是子类的func1函数,ptr1指向调用对象的开始
- ptr2并没有指向子类对象的开始,此时调用子类对象的func1函数,this指针指向中间的位置不对了,所以需要修正this指针,使之指向子类对象开始的地方