如果一个C++类中包含有虚函数,C++编译器在进行编译时,会通过动态联编机制,为这个类生成一个“虚函数表”。
我们通常把所有的方法都是纯虚函数的类,叫做:接口类。
class BasicTable
{
public:
virtual void Function1() = 0;
virtual void Function2() = 0;
};
对于这个接口类里面方法的实现,需要编写一个子类继承于接口类:
class ChildTable : public BasicTable
{
public:
void Function1()
{
printf("Function1.\n");
};
void Function2()
{
printf("Function2.\n");
};
};
我们在调用的时候,就会涉及到虚函数表:
int _tmain(int argc, _TCHAR* argv[])
{
BasicTable* pBT = new ChildTable;
pBT->Function1();
pBT->Function2();
delete pBT;
pBT = NULL;
return 0;
}
那么指针pBT具体是怎么调用到子类的Function方法,我们来追踪下内存信息:
指针地址为:0x00429800,我们看下指针这块内存的信息:
其实这块内存的前4位字节就是虚函数表的指针地址,这里涉及到一个知识点:大端,小端的内存存储位置:
Intel X86体系结构的CPU使用Little Endian(小字节序)来存储数字。例如有一个数字为0x11223344,那么这个数据在内存中序列的顺序是44 33 22 11,即内存的低地址存放这个数字的低位。与此相对应的,还有Big Endia(大字节序),如PowerPC,所以数字0x11223344,在内存中的存储的序列为 11 22 33 44。
所以这块内存地址正确为:0x01245748,我们看下这块内存的信息:
我们在右边的信息,看到Function的方法,并且前4个字节0x012410a0就是Function1的地址,后4个字节0x01241140就是Function2的地址,我们确认下是不是:
通过Vs监视窗体可以详细看到:
通过一遍的跟踪,对虚函数表具体的信息,有了叫深的理解,我们直接模拟虚函数表的调用,写份代码:
class BasicTable
{
public:
virtual void Function1() = 0;
virtual void Function2() = 0;
};
class ChildTable : public BasicTable
{
public:
void Function1()
{
printf("Function1.\n");
};
void Function2()
{
printf("Function2.\n");
};
};
int _tmain(int argc, _TCHAR* argv[])
{
BasicTable* pBT = new ChildTable;
pBT->Function1();
pBT->Function2();
//!<获取虚函数表指针
LPDWORD VtablePtr = (LPDWORD)(*((LPDWORD)pBT));
//!<拿到Function方法的指针
DWORD Fn_0 = *(VtablePtr + 0);
DWORD Fn_1 = *(VtablePtr + 1);
//!<函数类型声明
typedef void(_stdcall* FUN)();
FUN _Fun1 = (FUN)Fn_0;
FUN _Fun2 = (FUN)Fn_1;
//!<直接调用
_Fun1();
_Fun2();
delete pBT;
pBT = NULL;
return 0;
}
输出:
Function1.
Function2.
Function1.
Function2.
结论:含有虚函数的C++类,它的对象的this指针所指向的地址即是虚函数表指针的所在。