今天在运行一段代码时,发现运行结果与我预期的不一样,所以google了一把,这里对研究结果作下总结
#pragma once
#include <iostream>
using std::cout;
using std::endl;
class Base
{
public:
virtual void f()
{
cout<<"Base f"<<endl;
}
virtual void g()
{
cout<<"Base g"<<endl;
}
};
class Der1 : public Base
{
public:
void f()
{
cout<<"Der1 f"<<endl;
}
void g()
{
cout<<"Der1 g"<<endl;
}
};
class Der2 : public Base
{
public:
void f()
{
cout<<"Der2 f"<<endl;
}
void g()
{
cout<<"Der2 g"<<endl;
}
};
#include "test.h"
typedef void (Base::*fbase)();
typedef void (Der1::*fdera)();
typedef void (Der2::*fderb)();
//typedef void (Base::*g)();
//typedef void (*func)();
int main()
{
//Base* pbase = new Base;
//printf("instance addr = %p\n", pbase);
//f fbase= &Base::f;
//printf("Base::f addr = %p\n", fbase);
//g gbase= &Base::g;
//printf("Base::g addr = %p\n", gbase);
//func pf = (func)(*(int*)*(int*)(pbase));
//printf("f addr = %p\n", pf);
//pf();
//pf = (func)*((int*)*(int*)(pbase) + 1);
//printf("g addr = %p\n", pf);
//pf();
fbase f1 = &Base::f;
printf("Base::f addr = %p\n", f1);
fdera f2 = &Der1::f;
printf("Der1::f addr = %p\n", f2);
fderb f3 = &Der2::f;
printf("Der2::f addr = %p\n", f3);
return 0;
}
Der1和Der2是Base的派生类,并且两个类都重写了基类的两个虚函数f和g。对于main函数,我最初期望输出的是三个不同的指针值,但是运行结果输出了一样的值,运行结果如下:
结果出乎意料,于是搜了2篇帖子进行研究:
1、VC虚函数布局引发的问题 http://blog.csdn.net/zhanglei8893/article/details/6333751
2、C++虚函数表学习心得之由类实例地址到虚函数表再到虚函数地址中各种地址解析http://blog.csdn.net/chenqin158741019/article/details/8074945?reload
第一篇介绍了访问虚表的两种方式,一种是通过类对象实例地址转化来访问,另一种是通过vcall thunk技术;第二篇具体介绍了如何通过类对象实例访问虚表。
通过阅读这两篇文章,我对vcall thunk技术有了初步的概念。main函数中输出的3个地址值都对应一个调用Base::`vcall'{0}' (0表示虚函数表中第1个函数),所以这三个值并非真正的虚函数地址,真正的地址在vcall调用中计算,vcall的反汇编代码为:
00411082 mov eax,dword ptr [ecx]
00411084 jmp dword ptr [eax]
//我没有进行过反汇编,两条指令的地址值是根据我自己运行的结果更改的
ecx寄存器中保存着this指针,所以vcall是以this指针为基础,通过跳转偏移this指针若干个单位来寻找相应的虚函数地址(如第2个虚函数,jmp指令参数就变为dword ptr [eax+4],该指令的意思是调转到偏移this指针4字节的位置的那个双字指针指向的指令执行,不知道解释对不对,要去复习下汇编)。至于为什么可以这样查找,是因为虚函数表指针存放在类对象的开始位置,每个虚函数指针的大小又固定,所以只要知道要找的虚函数地址位于虚函数表的第几个slot(按照在类中的声明顺序),就能计算出虚函数地址。
虽然main函数中派生类的成员函数指针与基类值一样,但那并不是真正虚函数指针的值。在vcall中计算得到的值才是真正虚函数指针的值。
注:不知道我的有些理解是不是准确,但目前这样理解有助于我记忆,或许以后我有更深的理解