C++知识积累:如何获取虚函数表以及虚函数地址

       如果一个类中存在虚函数的话,那么编译器就会为这个类生成一个虚函数表,这个虚函数表中按照个虚函数的声明顺序存放了各个虚函数的地址,需要注意的是,这个虚函数表并不存在于类中,而对于这个类的每个对象,编译器都会为其生成一个透明不可见的指针,这个指针就是虚函数表指针,位于该对象内存中的开头,并指向了虚函数表的位置。换句话说,如果一个类中存在虚函数,假设声明一个对象a,在32位的编译情况下,对象a的内存布局中前4个字节中就存放了虚函数表的地址。

       因此,通过这前4个字节就可以找到虚函数表,而在虚函数表中,按照虚函数的声明顺序存放了各个虚函数地址。

       下面,将通过一个例子来阐述整个过程。

       首先定义一个类A,其中含有两个整型变量以及3个虚函数。

class A    //定义一个类A,类中有3个虚函数
{
public:
	int x;
	int y;
	virtual void f(){ cout << "f() called !" << endl; };
	virtual void f1(){ cout << "f1() called !" << endl; };
	virtual void f2(){ cout << "f2() called !" << endl; };
};

        然后在主函数中声明一个对象a,&a得到对象a的地址。前面说过,对象的内存布局中前4个字节数据为虚函数表的地址,因此将(&a)强制类型转换为(int *),来把从&a开始的4个字节当作一个整体,然后对其进行解引用,就相当于取出这4个字节中的数据,取出的数据就是虚函数表的地址了。因此虚函数表的地址就是*(int *)(&a)

int _tmain(int argc, _TCHAR* argv[])
{
	A a;
	cout << hex ;
	cout << "address of a : " << &a << endl;
	cout << "address of vtbl : " << *(int *)(&a) << endl;  //&a得到对象a的首地址,强制转换为(int *),意为将从&a开始的4个字节看作一个整体,而&a就是这个4字节整体的首地址,再通过“*”对这个整体进行解引用,最终得到由这4个字节数据组成的地址,也就是虚表的地址。
	……
}

       得到了虚函数表的地址,就能够找到虚函数表了。前面说过,虚函数表中存放的是各个虚函数的地址,在32位编译环境下,每个虚函数地址由4个字节构成,也就是说,从虚函数表的首地址开始,前4个字节构成了第一个声明的虚函数f()地址,接着4个字节构成了f1()的地址……

       因此,通过虚函数表的地址就可以很容易得到每个虚函数的地址了:假设虚函数表的地址为addr,那么与前面类似,先将其转换为(int *)型,然后解引用,就可以得到开头的4个字节了,即*(int *)*(int *)(&a),接着要想获得f1()的地址,既可以直接将虚函数表的地址偏移4个字节到f1,也可以将4个字节作为一个整体偏移1到f1,如下所示。

     int addr=*(int *)*(int *)(&a);  //虚表地址

	int faddr = *(int *)*(int *)(&a);//f()地址

    //int addr1 = *(int *)(*(int *)(&a)+4); //f1()地址,偏移4个字节直接到f1()
	int f1addr = *((int *)(*(int *)(&a))+ 1); // 或者以4个字节作为整体 偏移1到f1()

	int f2addr = *((int *)(*(int *)(&a)) + 2);  //f2()地址

       这样,就可以得到各个虚函数的地址了。为了验证整个过程的正确性,接下来就通过这些地址来调用相应函数来看运行结果。通过函数地址来调用函数可以以((void(*)(void))faddr)();实现,其中蓝色部分中第一个void指定了函数返回类型,第二个void指定了形参类型,红色部分为函数地址,最后的括号中放相应参数。如下:

((void(*)(void))faddr)();  //通过f()的地址调用f()

((void(*)(void))f1addr)(); //通过f1()的地址调用f1()

((void(*)(void))f2addr)(); //通过f2()的地址调用f2()

        运行结果如下:

C++知识积累:如何获取虚函数表以及虚函数地址_第1张图片

       根据结果可知,整个虚函数地址的获取方式是正确的。

你可能感兴趣的:(C/C++)