c++ - 虚拟表和 _vptr 存储方案

标签 c++ vtable vptr virtual-table

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

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();
}

谢谢!提前

最佳答案

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

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

所以,让我们从非虚拟函数开始。它们分为两类:静态和非静态。

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

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

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

enter image description here

因此,该对象包含它的数据和一个指向 vtable 的指针。 vtable 包含一个指向该类定义的每个虚函数的指针。然而,为什么我们需要如此多的间接性可能不是很明显。要理解这一点,让我们看下一个(稍微有点)更复杂的情况:该类的两个实例:

enter image description here

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

现在,让我们考虑派生/继承。例如,让我们将现有类重命名为“Base”,并添加一个派生类。由于我感觉很有想象力,我将其命名为“派生”。如上,基类定义了两个虚函数。派生类覆盖其中一个(但不是另一个):

enter image description here

当然,我们可以将两者结合起来,每个基类和/或派生类都有多个实例:

enter image description here

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

struct simple_base { 
    int a;
};

struct simple_derived : public simple_base {
    int b;
};

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

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

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

关于c++ - 虚拟表和 _vptr 存储方案,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/45887575/

相关文章:

c++ - 设计题: Holding class metadata for dynamic lookup

c++ - 椭圆旋转矩阵?

c++ - 如果类类型已知,是否使用虚拟分派(dispatch)?

c++ - C++中虚表的结构是怎样的?

c++ - 在 MSVC ABI 中,如何可靠地找到仅给出 (void*) 的 vtable?

c++ - bash 文件并多次运行 c++ 代码

c++ - 队列不是模板

c++ - C++ 程序中的虚拟表和虚拟指针的数量

c++ - 无法修复对 vtable 的 undefined reference

C++:虚拟指针的原型(prototype)