虚函数的本质

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

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

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

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

在下面的例子中,我用一个指针(函数指针),指向VPTR,最终,我会调到我想要的虚函数。
思路:
先定义一个函数指针:typedef void (*functionHandler)();

所以:
说白了,晚绑定就是“玩转指针”。

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

比如:

cDerivedcD;

cBase*pB = &cD

非虚继承:


虚继承:



我曾经有点纠结:
基类中有虚函数;派生类中重新实现了虚函数,同时也有新的虚函数;
那么在派生类对象中,_VPTR指向的_VTABLE是否同时有基类的虚函数,也有派生类的虚函数?
我想是这样的:派生类重新实现了虚函数,对基类虚函数进行了覆盖,所以在派生类对象中的_VTABLE全部都是派生类的虚函数。

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

//代码基于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;	
}


  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值