C++中虚函数的诞生,就是为了多态的实现。当子类对父类的虚函数进行了重写,在父类指针调用重写的虚函数时,如果父类指针(或引用)指向了父类的对象,则调用父类的虚函数,如果父类指针(或引用)指向了子类对象,则调用子类的虚函数。

要想了解多态的实现,就必须要知道虚函数表的构成。

【注:文章中代码测试环境为Win7 64位 VS2013】

首先,我们讨论单个含有虚函数的类,即不存在继承关系。

当我们的类中含有虚函数时,类实例化出来的对象,他的成员除了自己的成员变量外,还会多出一个指针。这个指针我们称为虚表指针,他所指向的是我们类对象所维护的虚表。虚表中保存的是类中所有虚函数的地址。代码如下:

classB{public:B():_val(1){}virtualvoidfun1(){cout<<"voidB::fun1()"<<endl;}virtualvoidfun2(){cout<<"voidB::fun2()"<<endl;}voidfun3(){cout<<"voidB::fun3()"<<endl;}private:int_val;};intmain(){Bb;cout<<sizeof(b)<<endl;//输出结果为8system("pause");return0;}

代码说明:构造函数给类成员变量赋值为1,方便内存查看,成员函数有两个虚函数,一个非虚函数,断点打在return 0;处。

然后我们切换到监视窗口,<如下>可以发现,对象b实际上维护了两个成员,"__vfptr"和"_val",在内存窗口中"&b",得到的是 0012cc74 ,即 __vfptr,下一个是 00000001,即我们的成员 _val。这也解释了为什么 sizeof(b) 的输出结果是8。不管有多少虚函数,类中只保留了一个虚表指针,添加再多的虚函数,也不会改变sizeof(b)的值

另外,可以看到的是虚表只保存虚函数地址,非虚函数,依旧是属于整个类而非对象。


我们再次查看 __vfptr 指向的空间

前两个是我虚函数的地址,也就是说,他们之间存在关系如下

为了验证,这里我重新封装一个函数指针,通过偏移,看能不能输出cout里面的内容。代码如下:

typedefvoid(*p_fun)();voidPrint_fun(p_fun*ppfun){for(inti=0;/*ppfun[i]!=NULL*/i<2;i++){ppfun[i]();}}voidtest(){Bb;Print_fun((p_fun*)(*(int*)&b));}

测试是可以输出我们函数内容的。多说一句,虚表中前面保存的都是虚函数的地址,最后结束项在不同编译器下是不一样的,在VS2013环境下,最后一项保存的地址是不可访问的,VS2008环境下,最后是以0x00000000结尾,即是NULL。所以打印函数Print_fun()中for循环条件我做了修改。当然可以更加直接的这样调用函数。

//((p_fun*)(*(int*)&b))[0]();//((p_fun*)(*(int*)&b))[1]();

接下来,我们看看包含虚函数重写的单继承中的虚表

这里给出单继承的测试代码

classA{public:A():_a_val(1){}virtualvoidfun1(){cout<<"voidA::fun1()"<<endl;}virtualvoidfun2(){cout<<"voidA::fun2()"<<endl;}protected:int_a_val;};classB:publicA{public:B():_b_val(2){}virtualvoidfun1(){cout<<"virtualB::fun1()"<<endl;}virtualvoidfun3(){cout<<"virtualB::fun3()"<<endl;}virtualvoidfun4(){cout<<"virtualB::fun4()"<<endl;}protected:int_b_val;};voidtest(){Bb;}

代码说明:父类 A 包含两个虚函数 fun1() 、fun2(),一个成员变量 _a_val ,构造成员变量为 1 ;子类 B 共有继承了 A ,重写函数 fun1() ,同时添加两个自己的虚函数 fun3()、fun4(),成员变量 _b_val ,构造为 2 。

接下来切换到调试窗口<如下图>,可以看到类B实例化对象 b 实际上维护了三个成员,"__vfptr"、"_a_val"、"_b_val",在内存窗口中“&b”,得到的是0x009bcd48,对应到监视窗口,即 __vfptr ,接下来是0x00000001,0x00000002,即成员变量_a_val,_b_val。如果在这里去sizeof(b),得到的结果应该是12。

接下来查看 __vfptr 指向的空间

监视中看到的是只有两个函数地址,但内存窗口中可以看到,前四个在内存中是在一起的,或者说很近。为了确认,使用刚刚的打印函数,不过需要改变一下循环次数,受编译器的限制,这里只能手动修改循环次数,看最多打印多少次是正常结束,而非程序崩溃。

typedefvoid(*p_fun)();voidPrint_fun(p_fun*ppfun){for(inti=0;/*ppfun[i]!=NULL*/i<4;i++){ppfun[i]();}}voidtest(){Bb;//((p_fun*)(*(int*)&b))[0]();//((p_fun*)(*(int*)&b))[1]();//((p_fun*)(*(int*)&b))[2]();//((p_fun*)(*(int*)&b))[3]();Print_fun((p_fun*)(*(int*)&b));}

测试得到,最多可以打印四次,打印结果如下:

*可以看到fun1()函数被子类 B 重写,fun2()函数继承自父类。得到结果监视窗口未显示虚函数fun3()和fun4()地址,但实际上子类新创建的虚函数地址也会保存到虚表当中,而且在单继承过程中,子类的虚函数和父类的虚函数是保存在同一虚表当中,并未对子类的虚函数创建独立的虚表。

即有下图关系:

接下来的多继承中的对象模型

首先给出测试代码,如下

classA{public:A():_a_val(1){}virtualvoidtest1(){cout<<"A::test1()"<<endl;}virtualvoidtest2(){cout<<"A::test2()"<<endl;}protected:int_a_val;};classB{public:B():_b_val(2){}virtualvoidtest1(){cout<<"B::test1()"<<endl;}virtualvoidtest3(){cout<<"B::test3()"<<endl;}protected:int_b_val;};classC{public:C():_c_val(3){}virtualvoidtest1(){cout<<"C::test1()"<<endl;}virtualvoidtest4(){cout<<"C::test4()"<<endl;}protected:int_c_val;};classD:publicA,publicB,publicC{public:D():_d_val(4){}virtualvoidtest1(){cout<<"D::test1()"<<endl;}virtualvoidtest5(){cout<<"D::test5()"<<endl;}protected:int_d_val;};voidtest(){Dd;}

代码说明:首先创造三个基类,类 A 包含两个虚函数fun1()、fun2(),类包含成员变量 _a_val ,构造为1;类 B 包含两个虚函数 fun1()、fun3(),类包含成员变量 _b_val,构造为2;类 C 包含两个虚函数 fun1()、fun4(),类包含成员变量 _c_val,构造为3;创建第四个类,作为派生类 D ,同时共有继承类A、类B、类C,包含虚函数fun1(),fun2(),fun1()函数对子类中的fun1()进行重写,同时包含成员变量_d_val。

接下来切换到调试窗口<如下图>,可以看到类 D 实例化对象 d 这里维护了七个成员,由于继承了三个类,因此这里有三个虚表指针"__vfptr"、同时包含继承自三个类的成员变量"_a_val"、"_b_val"、"_c_val"和自己本身的成员变量"_d_val"。在内存窗口中“&d”,得到的是0x013bdd04,对应到监视窗口,即继承的第一个类的虚表指针 __vfptr ,接下来是0x00000001,即成员变量_a_val,接下来依次类推,得到第二个类的虚表指针,和继承自第二个类的成员变量,第三个类的虚表指针,和继承自第三个类的成员变量,最后一项是子类 D 的成员变量。如果在这里去 sizeof(b),得到的结果应该是28。

不过这里有个问题,是子类 D 的虚函数地址在哪里。。这里我们打开多个内存窗口,同时把各个虚表指针指向的内容列出来。<如图>

可以看到,尽管监视窗口中,A的虚表指针下只有两项,但对应到内存中却有三项,可以推测,子类单独的虚函数地址是保存在了第一继承子类的虚函数表中,未覆盖的虚函数不会单独创建一块虚函数表。

除此之外,还应该可以看到,子类每继承一个含有虚函数的父类,就会多一个虚表指针,可能会同时维护多个虚表。

换句话说,存在如下图对应关系。


多提一点,子类继承了多个父类,父类虚表的地址不一定是连续的

这里依旧使用函数指针的方式去调用我的成员函数来加以验证。代码如下

typedefvoid(*p_fun)();voidPrint_fun(p_fun*ppfun){for(inti=0;ppfun[i]!=NULL;i++){ppfun[i]();}}voidtest(){Dd;Print_fun((p_fun*)(*(int*)&d));cout<<"-----------------------------------------"<<endl;Print_fun((p_fun*)(*((int*)&d+2)));cout<<"-----------------------------------------"<<endl;Print_fun((p_fun*)(*((int*)&d+4)));cout<<"-----------------------------------------"<<endl;}

打印结果如下:

由打印结果可见,子类专有的虚函数test5()的函数地址放在了第一个继承的虚表中,test1()函数均被子类 D 重写。







我们通过虚函数表理解C++ 中的对象模型,了解多态实际上是用虚函数实现覆盖,但通过上面的测试,可以发现,实现多态的同时,无疑会带来效率的下降(通过两次指针解引用才可以访问)。

除此之外应该看到的一点是,多态实现的过程是不安全的,尽管虚函数表的内容我们不能够随意修改,但永远可以被直接访问,这是不安全的一种直接表现。

关于菱形继承的对象模型和菱形虚拟继承的对象模型,会在下一篇中提到。


------------------------------------------muhuizz------------------------------------------

http://11331490.blog.51cto.com