C++ Virtual Table

概述

为了实现 C++ 的多态,C++ 使用了一种 动态绑定 的技术。实现 动态绑定 的方法有很多种,但是很多编译器都是用了类似的方案:虚表。本文介绍如何通过 虚表 来实现 动态绑定

注意:虚表 本身并不是 C++ 的标准,它是编译器实现 动态绑定 的一种方式。

虚表

什么是虚表

虚表(Virtual Table) 是一个 指针数组,里面存储的是 函数指针,指向 虚函数

虚表在什么阶段生成

虚表 内的条目,即 虚函数指针 的赋值发生在编译器的编译阶段,也就是说在代码的编译阶段,虚表 就可以构造出来了。

谁会拥有虚表

如果一个 包含 虚函数,那么它就会包含一个 虚表。并且,虚表 是和 绑定的,也就是说这个 的所有 对象 都共享这同一个 虚表

注意:需要指出的是,普通函数非虚函数,它的调用并不需要经过 虚表,所以 虚表 中的元素并不包括 普通函数 的函数指针。

示例

A 包含 虚函数,故类 A 拥有一个虚表。

1
2
3
4
5
6
7
8
9
class A {
public:
virtual void vfunc1();
virtual void vfunc2();
void func1();
void func2();
private:
int data1, data2;
};

对于这个类 A虚表 结构示意图如图 1 所示:

class vtable

虚表指针

同一个 的所有 对象 都使用同一个虚表。为了指定 对象虚表对象 内部包含一个指针,来指向自己所使用的 虚表,这个指针就是 虚表指针

虚表指针 是编译器自动创建的:为了让每个包含 虚表 的类的对象都拥有一个虚表指针,编译器在 中添加了一个指针 *__vptr 用来指向虚表。这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向自己所属的那个类的虚表。

A 以及它的对象的 虚表 结构示意图如图 2 所示:

class vtable

注意:同一个 的 不同的 对象*__vptr 的值是一样的,也就是它们都指向同一个地方。

动态绑定

什么是动态绑定

经过 虚表 调用 虚函数 的过程称为 动态绑定,其表现出来的现象称为 运行时多态
动态绑定 相对应的是 静态绑定,区别于传统的函数调用就是 静态绑定,即函数的调用在 编译阶段 就可以确定下来了。

那么,什么时候会执行函数的动态绑定?这需要符合以下三个条件。

  • 通过 指针 来调用函数。
  • 指针 upcast 向上转型。
  • 调用的是 虚函数

如果一个函数调用符合以上三个条件,编译器就会把该函数调用编译成 动态绑定,其函数的调用过程走的是通过 虚表 的机制。具体是怎么做的呢,下一节就是对它做了描述。

如何使用 虚表 实现 动态绑定

那么是如何通过 虚表 来实现 动态绑定 的呢?

我们先看下面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class A {
public:
virtual void vfunc1();
virtual void vfunc2();
void func1();
void func2();

private:
int data1, data2;
};

class B : public A {
public:
virtual void vfunc1();
void func1();

private:
int data3;
};

class C : public B {
public:
virtual void vfunc2();
void func2();

private:
int data1, data4;
};

它们的 虚表 结构示意图如图 3 所示:

class vtable

需要特别注意的是:如果子类 override 了基类的虚函数,就会为这个 override 的函数重新创建一个新的 虚函数,比如上例的 B::vfunc1() override 了 A::vfunc1().

总结

  • 虚表 是实现 动态绑定 的一种方式,这种方式被大多数编译器所采用,但并不是 C++ 的一个标准。
  • 虚表 是一个 指针数组,数组中的元素是 函数指针,会指向其继承的最近的一个类的 虚函数 (如果是基类,那就是它自己的虚函数)。
  • 虚表 是和 绑定的,一个 只有一个 虚表,这个 的所有的 对象 都会共享它。
  • 对象 使用 虚表指针 用来指向自己所属类的 虚表

参考