虚表保存的其实并不是虚函数的地址,而是他的到jmp地址。
上我们的操作代码
class A
{
public:
virtual void func1()
{
}
virtual void func2()
{
}
int a = 1;
};
class B
{
public:
virtual void func1()
{
}
virtual void func2()
{
}
int b = 2;
};
class C : public A, public B
{
public:
virtual void func1()
{
printf("C::virtual void func1()\n");
}
virtual void func3()
{
}
int c = 3;
};
int main()
{
C cc;
A* pa = &cc;
B* pb = &cc;
pa->func1();
pb->func1();
return 0;
}
问题一:C类有几个虚函数表。
问题一:C类的func3的虚函数地址存放在哪。
问题三:C类,A类与B类分别切片的时候虚函数func1地址不一样。
问题一:
进入调试模式
我们可以清楚的看见我们的cc对象中继承的两个父类,而各各父类都有着自己的虚函数表。
cc的大小为20=A类(8)+B类(8)+成员变量c(4);//20已最大对称数对齐。
问题二
C::func3()保存在哪个虚函数表中呢?是A类还是B类还是都保存一份?
准备工具:打印函数地址与该地址函数调用,因为这三个函数的参数返回值一样所以可以使用函数指针来寻找我们func3的地址存放。因为在vs中虚表的最后有一个nullptr做表结尾,所以我们循环结束条件为地址是否为空。
typedef void(*Table)();
void printftable(Table table[])
{
for (size_t i = 0; table[i] != nullptr; ++i)
{
printf("table[%d]:%p ", i, ble[i]);
ble[i]();
}
}
printf("A类的虚函数表:\n");
printftable((Table*)*(int*)pa);
printf("=====================================\n");
printf("B类的虚函数表:\n");
printftable((Table*)*(int*)pb);
来解释一下我们的实参:
运行程序:
根据运行结果我们找到了派生类的虚函数func3存放在A类虚函数表中,我们改变继承顺序
//class C : public A, public B
class C : public B, public A
会发现,我们的C类虚函数func3的存放于继承顺序有关。
问题三
在改一些代码
我们知道这里pa与pb的是cc的切片后的AB类指针。
pa和pb调用的func1都是被C类的func1重写的函数。所以调用打印的都是C::func1()。
回到问题一的调试模式,打开A与B的vfptr 。
既然打印的结果一样,但是我们看监视窗口中AB的虚函数表的func1函数地址不同,vs的bug吗?
但是上面问题二的打印也是说明了我们的切片后func1()的地址不同,但是调用的函数相同,不是AB基类的func1被C类重写了吗??这是为什么呢?
这里涉及了指针偏移的问题。首先需要看得懂汇编代码。
进入调试模式,对指针调用继续汇编查看
现在画图截图保存pa与pb的保存的地址
pa中call eax指令继续jmp跳转
继续jmp
pa的跳转调用中,是直接跳转到我们的C类func1函数中。
让我们看看pb怎么跳转函数的。
pb中call eax指令jmp跳转
继续jmp
ecx是什么?是调用函数的对象地址。sub的意思是地址减8个字节
继续jmp
继续jmp
会发现最后在sub-8后我们的jump
全局跳转图
pb最后跳转依旧会来到pa的call位置0x00061343证明了,最后func1只有一份代码。
来解释一些为什么要pa与pb的虚函数表func1地址不同呢?应为pb的虚表中并不保存func1的jmp地址,而我们需要在第一次jmp后对ecx-8其实就是对我们的this(pb)-8个字节,改变指向到pa的虚函数表来调用A::中的func1真正的jmp地址。为什么pb不保存重写后的func1jmp地址?我其实吧也也想问问。