虚函数的本质

一直不明白虚函数的本质,在看了<<Think in C++>>之后,豁然开朗,下面就自己总结一下。
 虚函数之所以能够做到动态调用,是因为程序在执行阶段才确定调用,也就是晚绑定。而早绑定在编译阶段就已经确定下一步该调用哪个函数。

 那么晚绑定又是如何实现的呢?
晚绑定的本质是:当实例化一个带有虚函数(继承下来的虚函数也可以)类对象时,编译器会生成一个VPTR指针和VTABLE表,VTABLE表中存放所有“虚函数地址”;VPTR指向VTABLE的首地址。不管对象如何被强换(子类转换为基类),还是在传引用或传指针的过程中,它的地址都不会变;只要我们握有对象的地址,就可以通过对象地址找到VPTR,通过VPTR找到VTABLE,通过VPTABLE找到虚函数,从而调用正确的虚函数。

VPTR的位置都一样,一般都在对象的开头。VTABLE其实就是一个函数指针的数组,VPTR正指向VTABLE的第一个元素(第一个虚函数);如果VPTR向后偏移一个位置,那么它应该指向了VTABLE中的第二个函数了。

(注:如果子类没有实现虚函数,会继承基类的虚函数,依然建立自己的虚函数表;如果子类有新的虚函数,会添加到虚函数表中)
(有点绕口,如果不理解,自己都会晕)

在下面的例子中,我用一个指针(函数指针),指向VPTR,最终,我会调到我想要的虚函数。
思路:
先定义一个函数指针:typedef void (*functionHandler)();
虚函数的本质_第1张图片
所以:
说白了,晚绑定就是“玩转指针”。

补充一点(很重要,不知道自己的理解是否正确):
为什么类成员虚函数能够实现“动态映射”呢?
成员函数保存在常量区,所有对象共用相同的成员函数;那么成员函数中的局部变量呢?局部变量在函数读入后临时生成,跳出成员函数后,局部变量被释放(局部变量的地址都是类偏移地址)。 
对于非虚函数继承,当派生类对象指针赋值给基类对象指针时,基类对象指针指向的是“派生类对象中的基类部分的地址”,当发生函数调用时,调用的是基类的函数。
(对于非虚函数继承,是不正确的用法,详见《Effective C++》Iterm 36)
对于虚函数继承,当派生类对象指针赋值给基类对象指针时,基类对象指针指向的是“虚函数表_VTABLE”;_VTABLE中的内容发生了覆盖(派生类类虚函数覆盖了基类虚函数),所以当发生函数调用时,调用的是派生类的虚函数。
文档上都说,对于非虚函数的“静态映射”是由于“静态绑定”, 我想是因为直接的函数调用;而对于“动态映射”的“晚绑定”,是因为通过_VTABLE中的函数地址(函数指针)间接调用。

比如:

cDerivedcD;

cBase*pB = &cD

非虚继承:

虚函数的本质_第2张图片

虚继承:

虚函数的本质_第3张图片


细节上的问题见下面代码注释,码农就用代码说话。

//代码基于64bit机器码
#include <iostream>
using namespace std;

class cBase
{
	int data;
	public:
	virtual void fun1() {cout<<"cBase::fun1()"<<endl;}
	virtual void fun2() {cout<<"cBase::fun2()"<<endl;}
	virtual void fun3(int) {cout<<"cBase::fun3()"<<endl;}
};

class cDerivedA:public cBase
{
	int dataA;
	public:
	void fun1() {cout<<"cDerivedA::fun1()"<<endl;}
	void fun2() {cout<<"cDerivedA::fun2()"<<endl;}
	virtual void fun4() {cout<<"cDerivedA::fun4()"<<endl;}
};

class cDerivedB:public cBase
{
	int dataB;
};

class cNonDerived
{
	int dataNd;
	public:
	void fun1() {cout<<"cNonDerived::fun1()"<<endl;}
	void fun2() {cout<<"cNonDerived::fun2()"<<endl;}
};


//typedef void (*functionHandler)(int);
typedef void (*functionHandler)();

//VPTR是一个指针,位置一般都在对象的开头。
//VPTR指向VTABLE的首地址。
functionHandler getFun (cBase* obj, unsigned long off)
{
    
	//vptr在指针对象obj的开头;
	//(int*)obj:首先将obj地址强转为(int*)obj
	//*(int*)obj:再解引用,得到vptr。vptr是一个地址。
	//(int*)*(int*)obj: 将地址vptr强转为(int*)。
	int *vptr = (int*)*(int*)obj;
	//将vptr转换为unsigned char *,这样做的目的是为了便于指向下一位置(后移8位)
	unsigned char *p = (unsigned char *)vptr;
	//后移8位
	p += sizeof(void*) * off;
	//再次强转回去(int*)*( int*)p,最后(functionHandler)(int*)*( int*)p得到function类型。
	return (functionHandler)(int*)*( int*)p;
	
	//也可以直接向下面这样,省去了中途转(unsigned char *),但是偏移位应该是off*2,因为这里是64bit的。
	//return (functionHandler)*((int*)*(int*)(obj)+off*2);
}

//多态调用
//虽然传入的是cBase类型的对象引用,但通过晚绑定调用到正确的方法
//这里不能传值,因为会发生对象切片
void foo(cBase& obj)
{
	obj.fun1();
	obj.fun2();
	obj.fun3(3);
}
	
int main()
{
	cBase cB;
	cDerivedA cDa;
	cDerivedB cDb;
	
	cBase *pB = new cBase;
	//gdb *pA 输出为 {<cBase> = {_vptr.cBase = 0x400eb0, data = 0}, dataA = 0}
	cBase *pA = &cDa;	
	//pA->fun1();
	
	/*********************************************************/
	//多态方法
	foo(*pA);	
	
	/*********************************************************/
    //每个虚函数类都有一个VTABLE
    //通过函数指针调用_vtable中函数	
	functionHandler f = getFun(pA, 0);
	(*f)();
	//移位调用下一虚函数
	f = getFun(pA, 1);
	(*f)();
	
	f = getFun(pA, 2);
	(*f)();

	f = getFun(pA, 3);
	(*f)();	

	delete pB;
	
	//对比gdb *pA,这里gdb 输出为{data = 0}, 没有_vptr...
	cNonDerived *pN = new cNonDerived;
	delete pN;	
}


你可能感兴趣的:(虚函数的本质)