一直不明白虚函数的本质,在看了<<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
非虚继承:虚继承:
//代码基于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; }