virtual table 和 _vptr 存储方案

virtual table and _vptr storage scheme

有人可以解释一下不同 class 的虚拟 table 是如何存储在内存中的吗?当我们使用指针调用函数时,它们如何使用地址位置调用函数?我们可以使用 class 指针获得这些虚拟 table 内存分配大小吗?我想看看虚拟 table 为 class 使用了多少内存块。怎么才能看到?

class Base
{
public:
    FunctionPointer *__vptr;
    virtual void function1() {};
    virtual void function2() {};
};

class D1: public Base
{
public:
    virtual void function1() {};
};

class D2: public Base
{
public:
    virtual void function2() {};
};
int main()
{
    D1 d1;
    Base *dPtr = &d1;
    dPtr->function1();
}

谢谢!提前

每个 class 都有一个指向函数列表的指针,它们在派生 classes 中的顺序相同,然后被覆盖的特定函数在列表中的那个位置发生变化.

当您使用基本指针类型指向时,指向的对象仍然具有正确的 _vptr。

基地的

 Base::function1()
 Base::function2()

D1 的

 D1::function1()
 Base::function2()

D2 的

 Base::function1()
 D2::function2()

进一步派生 drom D1 或 D2 将只在当前 2 下方的列表中添加它们的新虚函数。

调用虚函数时调用对应的index即可,function1就是index 0

所以你的电话

 dPtr->function1();

实际上是

 dPtr->_vptr[0]();

虚拟 table 应该在 class 的实例之间共享。更准确地说,它位于 "class" 级别,而不是实例级别。如果在其层次结构中存在虚函数和 classes.

,则每个实例都有实际具有指向虚拟 table 的指针的开销

table 本身至少是为每个虚函数保存指针所必需的大小。除此之外,它是一个实现细节,它是如何实际定义的。检查 here 以获取包含更多详细信息的 SO 问题。

要记住的第一点是免责声明:none 这实际上是由标准保证的。该标准说明了代码应该是什么样子以及它应该如何工作,但实际上并没有具体说明编译器需要如何实现这一点。

也就是说,基本上所有 C++ 编译器在这方面的工作方式都非常相似。

那么,让我们从非虚函数开始吧。它们有两种 classes:静态和非静态。

两者中较简单的是静态成员函数。静态成员函数几乎就像是 class 的 friend 的全局函数,只是它还需要 class 的名称作为函数名称的前缀。

非静态成员函数稍微复杂一些。它们仍然是直接调用的普通函数——但是它们被传递了一个隐藏的指针,指向调用它们的对象的实例。在函数内部,您可以使用关键字 this 来引用该实例数据。因此,当您调用类似 a.func(b); 的代码时,生成的代码与您为 func(a, b);

获得的代码非常相似

现在让我们考虑虚函数。这是我们进入虚表和虚表指针的地方。我们有足够的间接性,最好画一些图表看看它是如何布局的。这几乎是最简单的情况:一个 class 的一个实例具有两个虚函数:

因此,该对象包含其数据和指向 vtable 的指针。 vtable 包含指向每个由 class 定义的虚函数的指针。然而,我们可能不会立即明白为什么我们需要如此多的间接性。为了理解这一点,让我们看看下一个(非常轻微)更复杂的案例:class:

的两个实例

请注意 class 的每个实例如何拥有自己的数据,但它们共享相同的 vtable 和相同的代码——如果我们有更多实例,它们仍然会共享同一个 vtable在相同 class.

的所有实例中

现在,让我们考虑一下 derivation/inheritance。例如,让我们将现有的 class 重命名为 "Base",并添加派生的 class。鉴于我的想象力,我将其命名为"Derived"。如上,基class定义了两个虚函数。派生的 class 覆盖其中一个(但不是另一个):

当然,我们可以将两者结合起来,每个base and/or derived class:

都有多个实例

现在让我们更详细地研究一下。派生的有趣之处在于,我们可以将一个 pointer/reference 传递给一个派生对象 class 到一个编写的函数,该函数接收一个 pointer/reference 到基数 class,并且它仍然有效——但如果你调用虚函数,你会得到实际 class 的版本,而不是基础 class 的版本。那么,它是如何工作的呢?我们如何将派生 class 的实例视为基础 class 的实例,并且仍然有效?为此,每个派生对象都有一个 "base class subobject"。例如,让我们考虑这样的代码:

struct simple_base { 
    int a;
};

struct simple_derived : public simple_base {
    int b;
};

在这种情况下,当您创建 simple_derived 的实例时,您会得到一个包含两个 int 的对象:aba(base class 部分)位于内存中对象的开头,b(派生 class 部分)紧随其后。因此,如果将对象的地址传递给需要基 class 实例的函数,它会使用基 class 中存在的部分,编译器将其放置在对象中的偏移量与它们在基础 class 的对象中的偏移量相同,因此函数可以在不知道它正在处理派生 class 的对象的情况下操纵它们。同样,如果你调用一个虚函数,它只需要知道 vtable 指针的位置。就它而言,像 Base::func1 这样的东西基本上只是意味着它遵循 vtable 指针,然后使用一个指向函数的指针从那里开始的某个指定偏移量(例如,第四个函数指针)。

至少现在,我要忽略多重继承。它给图片增加了相当多的复杂性(尤其是当涉及到虚拟继承时)而你根本没有提到它,所以我怀疑你真的关心。

至于访问其中的任何一个,或以除简单调用虚函数以外的任何方式使用:您也许可以为特定的编译器想出一些东西——但不要指望它是可移植的.尽管调试器之类的东西经常需要查看这些东西,但涉及的代码往往非常脆弱且特定于编译器。

Jerry Coffin 给出的答案很好地解释了虚函数指针如何在 C++ 中实现运行时多态性。但是,我认为它无法回答 vtable 在内存中的存储位置。正如其他人指出的那样,这不是标准规定的。

不过,Martin Kysel 的精彩 blog post(s) 非常详细地介绍了虚拟表的存储位置。总结博客 post(s):

  1. 为每个 class(不是实例)创建一个具有虚函数的 vtable。这个 class 的每个实例都指向内存中的同一个 vtable
  2. 每个 vtable 都存储在生成的二进制文件的只读内存中
  3. vtable 中每个函数的反汇编存储在生成的 ELF 二进制文件的文本部分
  4. 试图覆盖位于只读内存中的 vtable,导致分段错误(如预期)

首先,下面的回答包含了你想知道的关于虚拟 table 的几乎所有内容:

如果您正在寻找更具体的内容(定期免责声明,这可能会在平台、编译器和 CPU 体系结构之间发生变化):

  1. 需要时,正在为 class 创建虚拟 table。 class 将只有一个虚拟 table 的实例,并且 class 的每个对象将有一个指向此虚拟 table 的内存位置的指针。虚拟 table 本身可以被认为是一个简单的指针数组。
  2. 当您将派生指针分配给基指针时,它还包含指向虚拟的指针table。这意味着基指针指向派生 class 的虚拟 table。编译器会将此调用定向到虚拟 table 中的偏移量,其中将包含来自派生 class.
  3. 的函数的实际地址
  4. 不是真的。通常在对象的开头,有一个指向虚拟 table 本身的指针。但这对你没有太大帮助,因为它只是一个指针数组,没有真正指示其大小。
  5. 把一个很长的答案简短化:对于确切的大小,您可以在 executable(或从它加载到内存的段中)中找到此信息。充分了解虚拟 table 的工作原理后,如果您知道代码、编译器和目标体系结构,就可以获得非常准确的估计。

    对于确切的大小,您可以在 executable 或从 executable 加载的内存段中找到此信息。 executable 通常是一个 ELF 文件,这种文件包含 运行 程序所需的信息。此信息的一部分是各种语言结构的符号,例如变量、函数和虚拟 table。对于每个符号,它包含它在内存中占用的大小。所以按钮行,你需要虚拟的符号名称table,以及足够的ELF知识才能提取你想要的东西。