C/C++ 虚函数实现的基本原理

1. 概述

简单地说,每一个含有虚函数(无论是其本身的,还是继承而来的)的类都至少有一个与之对应的虚函数表,其中存放着该类所有的虚函数对应的函数指针。

例: img

其中:

  • B 的虚函数表中存放着 B::foo 和 B::bar 两个函数指针。
  • D 的虚函数表中存放的既有继承自 B 的虚函数 B::foo,又有重写(override)了基类虚函数 B::bar 的 D::bar,还有新增的虚函数 D::quz。

提示:为了描述方便,本文在探讨对象内存布局时,将忽略内存对齐对布局的影响。

2. 虚函数表构造过程

从编译器的角度来说,B 的虚函数表很好构造,D 的虚函数表构造过程相对复杂。下面给出了构造 D 的虚函数表的一种方式(仅供参考): img

提示:该过程是由编译器完成的,因此也可以说:虚函数替换过程发生在编译时。

3. 虚函数调用过程

以下面的程序为例: img

编译器只知道 pb 是 B * 类型的指针,并不知道它指向的具体对象类型 :pb 可能指向的是 B 的对象,也可能指向的是 D 的对象。

但对于 “pb->bar ()”,编译时能够确定的是:此处 operator-> 的另一个参数是 B::bar(因为 pb 是 B * 类型的,编译器认为 bar 是 B::bar),而 B::bar 和 D::bar 在各自虚函数表中的偏移位置是相等的。

无论 pb 指向哪种类型的对象,只要能够确定被调函数在虚函数中的偏移值,待运行时,能够确定具体类型,并能找到相应 vptr 了,就能找出真正应该调用的函数。

提示:本人曾在 “C/C++ 杂记:深入理解数据成员指针、函数成员指针” 一文中提到:虚函数指针中的 ptr 部分为虚函数表中的偏移值(以字节为单位)加 1。

B::bar 是一个虚函数指针, 它的 ptr 部分内容为 9,它在 B 的虚函数表中的偏移值为 8(8+1=9)。

当程序执行到 “pb->bar ()” 时,已经能够判断 pb 指向的具体类型了:

  • 如果 pb 指向 B 的对象,可以获取到 B 对象的 vptr,加上偏移值 8((char*) vptr + 8),可以找到 B::bar。
  • 如果 pb 指向 D 的对象,可以获取到 D 对象的 vptr,加上偏移值 8((char*) vptr + 8) ,可以找到 D::bar。
  • 如果 pb 指向其它类型对象... 同理...

4. 多重继承

当一个类继承多个类,且多个基类都有虚函数时,子类对象中将包含多个虚函数表的指针(即多个 vptr),例: img

其中:D 自身的虚函数与 B 基类共用了同一个虚函数表,因此也称 B 为 D 的主基类(primary base class)。

虚函数替换过程与前面描述类似,只是多了一个虚函数表,多了一次拷贝和替换的过程。

虚函数的调用过程,与前面描述基本类似,区别在于基类指针指向的位置可能不是派生类对象的起始位置,以如下面的程序为例: img

5. 菱形继承

本文不讨论菱形继承的情形,个人觉得:菱形继承的复杂度远大于它的使用价值,这也是 C++ 让人又爱又恨的原因之一。

参考文章

https://www.linuxidc.com/Linux/2019-01/156152.htm