最近在逛B站的时候发现有候捷老师的课程,如获至宝。因此,跟随他的讲解又复习了一遍关于C++的内容,收获也非常的大,对于某些模糊的概念及遗忘的内容又有了更深的认识。
以下内容是关于虚函数表、虚函数指针,而C++中的动态绑定实现和这两个内容是分不开的。
虚函数表、虚指针
当一个类在实现的时候,如果存在一个或以上的虚函数时,那么这个类便会包含一张虚函数表。而当一个子类继承并重载了基类的虚函数时,它也会有自己的一张虚函数表。当我们在设计类的时候,如果把某个函数设置成虚函数时,也就表明我们希望子类在继承的时候能够有自己的实现方式;如果我们明确这个类不会被继承,那么就不应该有虚函数的出现。
下面是某个基类A的实现:
class
从下图中可以看到该类在内存中的存放形式,对于虚函数的调用是通过查虚函数表来进行的,每个虚函数在虚函数表中都存放着自己的一个地址,而如何在虚函数表中进行查找,则是通过虚指针来调用,在内存结构中它一般都会放在类最开始的地方,而对于普通函数则不需要通过查表操作。这张虚函数表是什么时候被创建的呢?它是在编译的时候产生,否则这个类的结构信息中也不会插入虚指针的地址信息。
以下例子包含了继承关系:
class
以上三个类在内存中的排布关系如下图所示:
- 对于非虚函数,三个类中虽然都有一个叫 func2 的函数,但他们彼此互不关联,因此都是各自独立的,不存在重载一说,在调用的时候也不需要进行查表的操作,直接调用即可。
- 由于子类B和子类C都是继承于基类A,因此他们都会存在一个虚指针用于指向虚函数表。注意,假如子类B和子类C中不存在虚函数,那么这时他们将共用基类A的一张虚函数表,在B和C中用虚指针指向该虚函数表即可。但是,上面的代码设计时子类B和子类C中都有一个虚函数 vfunc1,因此他们就需要各自产生一张虚函数表,并用各自的虚指针指向该表。由于子类B和子类C都对 vfunc1 作了重载,因此他们有三种不同的实现方式,函数地址也不尽相同,在使用的时候需要从各自类的虚函数表中去查找对应的 vfunc1 地址。
- 对于虚函数 vfunc2,两个子类都没有进行重载操作,所以基类A、子类B和子类C将共用一个 vfunc2,该虚函数的地址会分别保存在三个类的虚函数表中,但他们的地址是相同的。
- 从上图可以发现,在类对象的头部存放着一个虚指针,该虚指针指向了各自类所维护的虚函数表,再通过查找虚函数表中的地址来找到对应的虚函数。
- 对于类中的数据而言,子类中都会包含父类的信息。如上例中的子类C,它自己拥有一个变量 m_data1,似乎是和基类中的 m_data1 重名了,但其实他们并不存在联系,从存放的位置便可知晓。
关于动态绑定
在设计了以上三个类之后,我们就要开始对它们进行使用。
int
假如在程序中分别创建两个对象 a 和 b,a的创建是通过将b强制转化为类A得来的。对于 b.vfunc1() 的调用,应该没有太的疑问,它所调用的就是类B中的 vfunc1。而对于 a.vfunc1() 的调用,它虽然是强制转化后的结果,但并不能改变它是一个类A对象的事实,因此这里调用的便是类A中的 vfunc1,也就是上图中显示绿色的函数。
int
将程序改写成以上内容,pa 是一个类A的指针,但它指向的是一个类B的对象。在使用pa调用 vfunc1 的时候,程序发现pa是一个指针,并且现在正在调用一个虚函数叫做 vfunc1,这时通过 pa->vptr 这个虚指针到类B的虚函数中(上图的B vtbl)找对应的虚函数地址,找到该地址以后,就用相应的虚函数来进行调用,也就是调用上图所示的 B::vfunc1()。
pa是类A的指针,为什么查找的是类B的虚函数表?
只要某一个类X包含虚函数,无论是它的父类或者它本身拥有,那么这个类的对象都会包含一个虚指针vptr,至于vptr要指向哪张表,取决于类X它本身是否含有虚函数。此处,类B中存在虚函数,那么它就会拥有自己的一张虚函数表。pa指向的是一个类B的对象,因此 p-vptr 指代的是类B中虚指针,所以它查找的是类B的虚函数表
如何从虚函数表中查找到 vfunc1 的地址?
虚函数表中的内容是在编译的时候确定的,通过以下方式进行查找 (* p->vptr[n] )(p) 或者 (* (p->vptr)[n] )(p),它的解读是:通过类对象指针p找到虚指针vptr,再查找到虚函数表中的第n个内容,并将他作为函数指针进行调用,调用时的入参是p(式子中的第二个p),而这个p就是隐藏的this指针,这里的n也是在编译的时候确定的。
int
再将程序修改成以上内容,对于 p2->vfunc1() 的调用和上文所述一致,它调用的是 B::vfunc1 函数。而对于 p1->vfunc1() 的调用,同样通过上面的方法可知, p1->vptr 它所指向的是类A的虚函数表,因此它调用的是 A::vfunc1 函数。
通过以上内容,我们可以知道在使用基类指针调用虚函数的时候,它能够根据所指的类对象的不同来正确调用虚函数。而这些能够正常工作,得益于虚指针和虚函数表的引入,使得在程序运行期间能够动态调用函数。
动态绑定有以下三项条件要符合:
- 使用指针进行调用
- 指针属于up-cast后的
- 调用的是虚函数
与动态绑定相对应的是静态绑定,它属于编译的时候就确定下来的,如上文的非虚函数,他们是类对象直接可调用的,而不需要任何查表操作,因此调用的速度也快于虚函数。
参考
- https://www.bilibili.com/video/av48240648?p=1