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
的对象:a
和 b
。 a
(base class 部分)位于内存中对象的开头,b
(派生 class 部分)紧随其后。因此,如果将对象的地址传递给需要基 class 实例的函数,它会使用基 class 中存在的部分,编译器将其放置在对象中的偏移量与它们在基础 class 的对象中的偏移量相同,因此函数可以在不知道它正在处理派生 class 的对象的情况下操纵它们。同样,如果你调用一个虚函数,它只需要知道 vtable 指针的位置。就它而言,像 Base::func1
这样的东西基本上只是意味着它遵循 vtable 指针,然后使用一个指向函数的指针从那里开始的某个指定偏移量(例如,第四个函数指针)。
至少现在,我要忽略多重继承。它给图片增加了相当多的复杂性(尤其是当涉及到虚拟继承时)而你根本没有提到它,所以我怀疑你真的关心。
至于访问其中的任何一个,或以除简单调用虚函数以外的任何方式使用:您也许可以为特定的编译器想出一些东西——但不要指望它是可移植的.尽管调试器之类的东西经常需要查看这些东西,但涉及的代码往往非常脆弱且特定于编译器。
Jerry Coffin 给出的答案很好地解释了虚函数指针如何在 C++ 中实现运行时多态性。但是,我认为它无法回答 vtable 在内存中的存储位置。正如其他人指出的那样,这不是标准规定的。
不过,Martin Kysel 的精彩 blog post(s) 非常详细地介绍了虚拟表的存储位置。总结博客 post(s):
- 为每个 class(不是实例)创建一个具有虚函数的 vtable。这个 class 的每个实例都指向内存中的同一个 vtable
- 每个 vtable 都存储在生成的二进制文件的只读内存中
- vtable 中每个函数的反汇编存储在生成的 ELF 二进制文件的文本部分
- 试图覆盖位于只读内存中的 vtable,导致分段错误(如预期)
首先,下面的回答包含了你想知道的关于虚拟 table 的几乎所有内容:
如果您正在寻找更具体的内容(定期免责声明,这可能会在平台、编译器和 CPU 体系结构之间发生变化):
- 需要时,正在为 class 创建虚拟 table。 class 将只有一个虚拟 table 的实例,并且 class 的每个对象将有一个指向此虚拟 table 的内存位置的指针。虚拟 table 本身可以被认为是一个简单的指针数组。
- 当您将派生指针分配给基指针时,它还包含指向虚拟的指针table。这意味着基指针指向派生 class 的虚拟 table。编译器会将此调用定向到虚拟 table 中的偏移量,其中将包含来自派生 class.
的函数的实际地址
- 不是真的。通常在对象的开头,有一个指向虚拟 table 本身的指针。但这对你没有太大帮助,因为它只是一个指针数组,没有真正指示其大小。
- 把一个很长的答案简短化:对于确切的大小,您可以在 executable(或从它加载到内存的段中)中找到此信息。充分了解虚拟 table 的工作原理后,如果您知道代码、编译器和目标体系结构,就可以获得非常准确的估计。
对于确切的大小,您可以在 executable 或从 executable 加载的内存段中找到此信息。 executable 通常是一个 ELF 文件,这种文件包含 运行 程序所需的信息。此信息的一部分是各种语言结构的符号,例如变量、函数和虚拟 table。对于每个符号,它包含它在内存中占用的大小。所以按钮行,你需要虚拟的符号名称table,以及足够的ELF知识才能提取你想要的东西。
有人可以解释一下不同 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
的对象:a
和 b
。 a
(base class 部分)位于内存中对象的开头,b
(派生 class 部分)紧随其后。因此,如果将对象的地址传递给需要基 class 实例的函数,它会使用基 class 中存在的部分,编译器将其放置在对象中的偏移量与它们在基础 class 的对象中的偏移量相同,因此函数可以在不知道它正在处理派生 class 的对象的情况下操纵它们。同样,如果你调用一个虚函数,它只需要知道 vtable 指针的位置。就它而言,像 Base::func1
这样的东西基本上只是意味着它遵循 vtable 指针,然后使用一个指向函数的指针从那里开始的某个指定偏移量(例如,第四个函数指针)。
至少现在,我要忽略多重继承。它给图片增加了相当多的复杂性(尤其是当涉及到虚拟继承时)而你根本没有提到它,所以我怀疑你真的关心。
至于访问其中的任何一个,或以除简单调用虚函数以外的任何方式使用:您也许可以为特定的编译器想出一些东西——但不要指望它是可移植的.尽管调试器之类的东西经常需要查看这些东西,但涉及的代码往往非常脆弱且特定于编译器。
Jerry Coffin 给出的答案很好地解释了虚函数指针如何在 C++ 中实现运行时多态性。但是,我认为它无法回答 vtable 在内存中的存储位置。正如其他人指出的那样,这不是标准规定的。
不过,Martin Kysel 的精彩 blog post(s) 非常详细地介绍了虚拟表的存储位置。总结博客 post(s):
- 为每个 class(不是实例)创建一个具有虚函数的 vtable。这个 class 的每个实例都指向内存中的同一个 vtable
- 每个 vtable 都存储在生成的二进制文件的只读内存中
- vtable 中每个函数的反汇编存储在生成的 ELF 二进制文件的文本部分
- 试图覆盖位于只读内存中的 vtable,导致分段错误(如预期)
首先,下面的回答包含了你想知道的关于虚拟 table 的几乎所有内容:
如果您正在寻找更具体的内容(定期免责声明,这可能会在平台、编译器和 CPU 体系结构之间发生变化):
- 需要时,正在为 class 创建虚拟 table。 class 将只有一个虚拟 table 的实例,并且 class 的每个对象将有一个指向此虚拟 table 的内存位置的指针。虚拟 table 本身可以被认为是一个简单的指针数组。
- 当您将派生指针分配给基指针时,它还包含指向虚拟的指针table。这意味着基指针指向派生 class 的虚拟 table。编译器会将此调用定向到虚拟 table 中的偏移量,其中将包含来自派生 class. 的函数的实际地址
- 不是真的。通常在对象的开头,有一个指向虚拟 table 本身的指针。但这对你没有太大帮助,因为它只是一个指针数组,没有真正指示其大小。
- 把一个很长的答案简短化:对于确切的大小,您可以在 executable(或从它加载到内存的段中)中找到此信息。充分了解虚拟 table 的工作原理后,如果您知道代码、编译器和目标体系结构,就可以获得非常准确的估计。
对于确切的大小,您可以在 executable 或从 executable 加载的内存段中找到此信息。 executable 通常是一个 ELF 文件,这种文件包含 运行 程序所需的信息。此信息的一部分是各种语言结构的符号,例如变量、函数和虚拟 table。对于每个符号,它包含它在内存中占用的大小。所以按钮行,你需要虚拟的符号名称table,以及足够的ELF知识才能提取你想要的东西。