C++ 虚函数表
目录

1. vtbl由来

多态是OOP的重要方式。C++以下列方法支持多态(B extends A):

  1. 隐式转化操作,基类指针指向派生类对象:A *p = new B
  2. virtual function机制:A->show();
  3. dynamic_casttypeid运算符:if (B *pc = dynamic_cast<B*>(p)) ...

通过virtual关键字声明的函数会在运行时引发动态绑定(dynamic binding/runtime binding)。 现在分析一下编译器为这种特性的实现提供的关键数据结构:虚函数表(virtual table, vtbl)

编译器为每个包含虚函数的类或从包含虚函数的类派生的类创建一个vtbl, 并在这个类中增加一个隐藏的虚函数指针(virtual pointer, vptr)指向刚才的vtbl。 vptr一般位于类的对象中最开始的位置,以便保证快速取得虚表(不是标准)。 vptr的设定(setting)和重置(resetting)都由每一个class的contructor, destructor和copy assignment运算符自动完成。 进行如下实验:

class A
{
void show();
long a;
};

class B
{
virtual void show();
long b;
};

class C: public B
{
};

int main(int argc, char *argv[])
{
std::cout << sizeof(A) << std::endl;
std::cout << sizeof(B) << std::endl;
std::cout << sizeof(C) << std::endl;
return 0;
}

在我的64位Linux上输出:

8
16
16

显然B类比A类多了一个void *的长度,C类继承自B类因此也有vtbl。

一个class object需要多少内存?

  • non-static data members之和,加上其对齐所占字节
  • 支持virtual(虚函数或虚类)所产生的额外负担(overhead),在这里即vptr指针。

如果class object没有non-static data members,那么编译器会强制申请1个字节, 并把对象指向那个字节,这便于通过地址区分不同对象。 这和C是不同的,C中不含数据的struct大小为0。

2. vtbl中有什么呢?

简单地讲,vtbl是一个保存了类中所有虚函数地址的数组。 这些虚函数的地址存放顺序和虚函数声明的顺序是一样的。 如果派生类没有对基类的某个虚函数进行重写,那么vtbl里还是使用基类虚函数的地址。

从参考文章里找了三张图,第一张是基类Base,包含三个虚函数:f(),g(),h():

第二个是无覆盖的派生类,增加里三个虚函数:f1(),g1(),h1()。 如果派生类没有对基类的某个虚函数进行重写,那么vtbl里还是使用基类虚函数的地址, 并且新增的虚函数添加在基类虚函数的后面。

第三个是有覆盖的派生类,覆盖了基类f()函数,增加了两个虚函数:g1(),h1()。 如果派生类重写了某个虚函数,那么vtbl里使用的是派生类虚函数的地址。

3. 多重继承下的虚函数

多重继承下虚表比一般情况下更复杂。假设有三个基类:

  • Base1: f(), g(), h()
  • Base2: f(), g(), h()
  • Base3: f(), g(), h()

现在派生类继承这三个基类,并增加了两个虚函数:f1(),g1()。 则派生类为每个基类都建一个虚表,并把派生类新增加的虚函数添加到第一个虚表中。

如果派生类重写了虚函数f(),并增加了一个虚函数g1(),则其结构为:

4. RTTI

为了支持运行时类型信息(Runtime Type Information),每一个class要关联type_info object, 这个指向type_info object的指针有时也保存在vtbl的第一个slot。

参考:

发表评论