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++ 的一个标准。虚表
是一个指针数组
,数组中的元素是函数指针
,会指向其继承的最近的一个类的虚函数
(如果是基类,那就是它自己的虚函数)。虚表
是和类
绑定的,一个类
只有一个虚表
,这个类
的所有的对象
都会共享它。对象
使用虚表指针
用来指向自己所属类的虚表
。