基类中的虚拟继承和空vtable

Virtual inheritance and empty vtable in base class

本文关键字:vtable 继承 虚拟 基类      更新时间:2023-10-16

有这样的代码:

#include <iostream>
class Base
{
   int x;
};
class Derived : virtual public Base
{
   int y;
};
int main()
{
    std::cout << sizeof(Derived) << std::endl; // prints 12
    return 0;   
}

我读到,当某个类实际上是继承的时,就会为Derived类创建空的vtable,所以内存布局如下:

Derived::ptr to empty vtable
Derived::y
Base::x

并且它是12个字节。问题是,如果没有任何虚拟方法,这个vtable的目的是什么?它是如何使用的?

Derived需要某种方法来知道Base子对象在哪里。使用虚拟继承,基类相对于派生类的位置不是固定的:它可能位于整个对象中的任何位置。

考虑一个涉及钻石继承的更典型的例子。

struct A
{
    int a;
};
struct B1 : virtual A
{
    int b1;
};
struct B2 : virtual A
{
    int b2;
};
struct C : B1, B2
{
    int c;
};

这里,B1B2实际上都是从A派生的,所以在C中,正好有一个子对象AB1B2都需要知道如何找到A子对象(这样他们就可以访问a成员变量,或者如果我们要定义A的其他成员)。

这就是vtable在这种情况下的用途:B1B2都将有一个包含A子对象偏移量的vtable。


为了演示编译器可以如何实现上述菱形继承示例,请考虑以下由Visual C++11 Developer Preview生成的类布局和虚拟表。

class A size(4):
        +---
 0      | a
        +---
class B1        size(12):
        +---
 0      | {vbptr}
 4      | b1
        +---
        +--- (virtual base A)
 8      | a
        +---
class B2        size(12):
        +---
 0      | {vbptr}
 4      | b2
        +---
        +--- (virtual base A)
 8      | a
        +---
class C size(24):
        +---
        | +--- (base class B1)
 0      | | {vbptr}
 4      | | b1
        | +---
        | +--- (base class B2)
 8      | | {vbptr}
12      | | b2
        | +---
16      | c
        +---
        +--- (virtual base A)
20      | a
        +---

以及以下vtables:

B1::$vbtable@:
 0      | 0
 1      | 8 (B1d(B1+0)A)
B2::$vbtable@:
 0      | 0
 1      | 8 (B2d(B2+0)A)
C::$vbtable@B1@:
 0      | 0
 1      | 20 (Cd(B1+0)A)
C::$vbtable@B2@:
 0      | 0
 1      | 12 (Cd(B2+0)A)

注意,偏移量相对于vtable的地址,并且注意,对于为CB1B2子对象生成的两个vtable,偏移量是不同的。

(还要注意,这完全是一个实现细节——其他编译器可能会以不同的方式实现虚拟函数和基。这个例子演示了它们的一种实现方式,而且它们通常是以这种方式实现的。)

为了实现虚拟函数,C++使用了一种特殊形式的后期绑定,称为虚拟表。虚拟表是用于以动态/后期绑定方式解析函数调用的函数的查找表。虚拟表有时使用其他名称,如"vtable"、"虚拟函数表"、"虚方法表"或"调度表"。

虚拟表实际上非常简单。首先,每个使用虚拟函数的类(或从使用虚拟函数类派生的类)都有自己的虚拟表。此表只是编译器在编译时设置的静态数组。虚拟表为类的对象可以调用的每个虚拟函数包含一个条目。该表中的每个条目都只是一个函数指针,指向该类可访问的最派生的函数。

  • 使用虚拟函数(或从类派生)的每个类使用虚拟函数)的虚拟表秘密数据成员
  • 此表由编译器在编译时设置
  • 虚拟表包含一个条目作为每个条目的函数指针类的对象可以调用的虚拟函数
  • 虚拟表存储指向纯虚拟函数的NULL指针

即使对于具有虚拟基类的类,也会创建虚拟表。在这种情况下,vtable具有指向基类的共享实例的指针,以及指向类的虚拟函数(如果有的话)的指针

如果执行dynamic_cast<派生*>(ptr_to_obj),它将使用vtable指针来确定ptr_to_obj是否引用了Derived。每个涉及虚拟方法或继承的类都需要一个vtable,并且每个类都需要不同的vtable才能支持dynamic_cast<>。即使它不包含任何指向方法的指针,它仍然用于标识对象的类型。