一、概述C++为了实现C++的多态性,采用了动态绑定技术。该技术的核心是虚函数表(以下简称虚表)。二、类的虚表每个包含虚函数的类都包含一个虚表。我们知道,当一个类(A)继承另一个类(B)时,A类将继承对B类函数的调用权。所以如果一个基类包含虚函数,那么它的继承类也可以调用这些虚函数。也就是说,如果一个类继承了一个包含虚函数的基类,那么这个类也有自己的虚表。让我们看看下面的代码。A类包含虚函数vfunc1和vfunc2。由于A类包含虚函数,所以A类有一个虚表。A类{public:virtualvoidvfunc1();虚空vfunc2();voidfunc1();voidfunc2();private:intm_data1,m_data2;};A类的虚表如图1所示。图1:A类的虚表示意图虚表是一个指针数组,它的元素是指向虚函数的指针,每个元素对应一个函数指针一个虚函数。需要指出的是,普通函数是非虚函数,它们的调用不需要经过虚表,所以虚表的元素不包括普通函数的函数指针。虚表中的入口,即虚函数指针的赋值发生在编译器的编译阶段,也就是说在代码的编译阶段,就可以构造虚表。3、虚表指针虚表是属于类的,不属于具体的对象。一个类只需要一个虚拟表。同一类的所有对象都使用相同的vtable。为了指定对象的虚表,对象包含一个虚表指针指向自己使用的虚表。为了让包含虚表的类的每个对象都有一个虚表指针,编译器添加了一个指向类的指针,*__vptr,以指向虚表。这样,类的对象在创建的时候,就有了this指针,this指针的值会自动设置为指向类的虚表。图2:对象及其虚表正如上面所指出的,如果一个继承类的基类包含虚函数,那么这个继承类也有自己的虚表,所以这个继承类的对象也包含一个虚表指针,用来指向它是一个虚拟表。四、动态绑定说到这里,大家一定很好奇C++是如何使用虚表和虚表指针来实现动态绑定的。我们先看下面的代码。A类{public:virtualvoidvfunc1();虚空vfunc2();voidfunc1();voidfunc2();private:intm_data1,m_data2;};B类:公共A{公共:虚拟无效vfunc1();voidfunc1();private:intm_data3;};C类:publicB{public:virtualvoidvfunc2();voidfunc2();private:intm_data1,m_data4;};A类是基类,B类继承A类,C类继承B类。A类、B类、C类,它们的对象模型如下图3所示。图3:A类、B类、C类的对象模型由于这三个类都有虚函数,所以编译器会为每个类创建一个虚表,即A类的虚表(Avtbl),类虚表B类(Bvtbl),C类(Cvtbl)的虚表。A类、B类、C类的对象都有一个虚表指针*__vptr,用来指向自己类的虚表。A类包含两个虚函数,所以Avtbl包含两个指针,分别指向A::vfunc1()和A::vfunc2()。B类继承自A类,所以B类可以调用A类的函数,但是由于B类重写了B::vfunc1()函数,所以B类vtbl的两个指针分别指向B::vfunc1()和A::vfunc2()。C类继承自B类,所以C类可以调用B类的函数,但是由于C类重写了C::vfunc2()函数,所以C类vtbl的两个指针指向B::vfunc1()(指向最近类的继承函数)和C::vfunc2()。图3虽然看起来有点复杂,但是只要把握住“对象的虚表指针是用来指向自己类的虚表,而虚表中的指针会指向虚函数”的特点它所继承的最近类的对象模型”,那么你就可以快速在脑海中画出这些类的对象模型。非虚函数的调用不需要经过虚表,所以虚表中的指针也不需要指向这些函数。假设我们定义了一个B类的对象bObject,由于bObject是B类的对象,所以bObject中包含了一个指向B类虚表的虚表指针。intmain(){BbObject;现在,我们声明一个类A的指针p指向对象bObject。虽然p是基类的指针只能指向基类的部分,但是虚表指针也属于基类的部分,所以p可以访问对象bObject的虚表指针。bObject的虚表指针指向B类的虚表,所以p可以访问B的vtbl。如图3所示。intmain(){BbObject;A*p=&bObject;当我们使用p调用vfunc1()函数时,会发生什么?intmain(){BbObject;A*p=&bObject;p->vfunc1();}当程序执行p->vfunc1()时,会发现p是一个指针,要调用的函数是虚函数。Next将执行以下步骤。首先根据虚表指针p->__vptr访问对象bObject对应的虚表。虽然指针p是基类A*类型,但是*__vptr也是基类的一部分,所以可以通过p->__vptr访问对象对应的虚表。然后在虚表中查找被调用函数对应的表项。由于可以在编译阶段构建虚拟表,因此可以根据调用的函数定位到虚拟表中对应的表项。对于p->vfunc1()的调用,Bvtbl的第一项就是vfunc1对应的entry。最后根据在虚表中找到的函数指针,调用函数。从图3可以看出,Bvtbl的第一项指向B::vfunc1(),所以p->vfunc1()本质上是调用了B::vfunc1()函数。如果p指向A类的对象怎么办?intmain(){一个对象;A*p=&aObject;p->vfunc1();aObject在创建时,其虚表指针__vptr已经被设置为指向Avtbl,所以p->__vptr指向Avtbl。Avtbl中vfunc1的条目指向A::vfunc1()函数,因此p->vfunc1()本质上将调用A::vfunc1()函数。以上调用函数的三个步骤可以用下面的表达式来表示:(*(p->__vptr)[n])(p)可以看出,通过使用这些虚函数表,即使基址的指针类是用来调用函数的,在运行中也能正确调用实际对象的虚函数。我们把通过虚表调用虚函数的过程称为动态绑定,它表现出来的现象称为运行时多态。动态绑定不同于传统的函数调用,我们称之为静态绑定,即函数调用在编译阶段就可以确定。那么,什么时候进行函数的动态绑定呢?这需要满足以下三个条件。通过指针调用函数指针upcast(从继承类到基类的转换称为upcast,什么是upcast,可以参考本文的参考资料)。如果一个函数调用满足以上三个条件,编译器就会将该函数调用编译成动态绑定,函数调用过程会通过虚表走上述机制。五、小结封装、继承、多态是面向对象设计的三大特征,而多态可以说是面向对象设计的关键。C++通过虚函数表实现了虚函数与对象的动态绑定,从而构筑了C++面向对象程序设计的基石。参考资料《C++ Primer》第三版,中文版,潘爱民等译。http://www.learncpp.com/cpp-tutorial/125-the-virtual-table/侯捷《C++最佳编程实践》视频,极客课堂,2015UpcastingandDowncasting,http://www.bogotobogo.com/cplusplus/upcasting_downcasting.php
