C++ Virtual Table
概述
为了实现 C++ 的多态,C++ 使用了一种 动态绑定 的技术。实现 动态绑定 的方法有很多种,但是很多编译器都是用了类似的方案:虚表。本文介绍如何通过 虚表 来实现 动态绑定。
注意:虚表 本身并不是 C++ 的标准,它是编译器实现 动态绑定 的一种方式。
虚表
什么是虚表
虚表(Virtual Table) 是一个 指针数组,里面存储的是 函数指针,指向 虚函数。
虚表在什么阶段生成
虚表 内的条目,即 虚函数指针 的赋值发生在编译器的编译阶段,也就是说在代码的编译阶段,虚表 就可以构造出来了。
谁会拥有虚表
如果一个 类 包含 虚函数,那么它就会包含一个 虚表。并且,虚表 是和 类 绑定的,也就是说这个 类 的所有 对象 都共享这同一个 虚表。
注意:需要指出的是,
普通函数即非虚函数,它的调用并不需要经过虚表,所以虚表中的元素并不包括普通函数的函数指针。
示例
类 A 包含 虚函数,故类 A 拥有一个虚表。
1 | class A { |
对于这个类 A 的 虚表 结构示意图如图 1 所示:
虚表指针
同一个 类 的所有 对象 都使用同一个虚表。为了指定 对象 的 虚表,对象 内部包含一个指针,来指向自己所使用的 虚表,这个指针就是 虚表指针。
虚表指针 是编译器自动创建的:为了让每个包含 虚表 的类的对象都拥有一个虚表指针,编译器在 类 中添加了一个指针 *__vptr 用来指向虚表。这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向自己所属的那个类的虚表。
类 A 以及它的对象的 虚表 结构示意图如图 2 所示:
注意:同一个
类的 不同的对象的*__vptr的值是一样的,也就是它们都指向同一个地方。
动态绑定
什么是动态绑定
经过 虚表 调用 虚函数 的过程称为 动态绑定,其表现出来的现象称为 运行时多态。
与 动态绑定 相对应的是 静态绑定,区别于传统的函数调用就是 静态绑定,即函数的调用在 编译阶段 就可以确定下来了。
那么,什么时候会执行函数的动态绑定?这需要符合以下三个条件。
- 通过
指针来调用函数。 - 指针 upcast 向上转型。
- 调用的是
虚函数。
如果一个函数调用符合以上三个条件,编译器就会把该函数调用编译成 动态绑定,其函数的调用过程走的是通过 虚表 的机制。具体是怎么做的呢,下一节就是对它做了描述。
如何使用 虚表 实现 动态绑定
那么是如何通过 虚表 来实现 动态绑定 的呢?
我们先看下面的代码
1 | class A { |
它们的 虚表 结构示意图如图 3 所示:
需要特别注意的是:如果子类 override 了基类的虚函数,就会为这个 override 的函数重新创建一个新的 虚函数,比如上例的 B::vfunc1() override 了 A::vfunc1().
总结
虚表是实现动态绑定的一种方式,这种方式被大多数编译器所采用,但并不是 C++ 的一个标准。虚表是一个指针数组,数组中的元素是函数指针,会指向其继承的最近的一个类的虚函数(如果是基类,那就是它自己的虚函数)。虚表是和类绑定的,一个类只有一个虚表,这个类的所有的对象都会共享它。对象使用虚表指针用来指向自己所属类的虚表。