2017-08-25 50 views
5

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

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

谢谢!提前

+0

[Wikipedia](https://en.wikipedia.org/wiki/Virtual_method_table) – Barmar

+4

这个vtable是依赖于实现的,没有标准的方法来访问它。 – Barmar

+0

因此,@Barmar有什么方法可以访问vtable或查看使用了多少内存vtable? –

回答

5

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

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

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

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

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

现在让我们考虑一下虚拟功能。这里是我们进入vtable和vtable指针的地方。我们有足够的间接性,可能最好绘制一些图表来看看它是如何布置的。这几乎是最简单的例子:有两个虚函数一类的一个实例:

enter image description here

所以,对象包含其数据和一个指向虚函数表。 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秒的对象:aba(基类部分)位于内存中对象的开始处,并且b(派生类部分)紧跟其后。因此,如果将对象的地址传递给期望基类实例的函数,则它将使用基类中存在的部分,编译器将相同的偏移量放置在对象中, d在基类的一个对象中,所以函数可以在不知道它处理派生类的对象的情况下操纵它们。同样,如果你调用一个虚拟函数,所有它需要知道的是vtable指针的位置。就它而言,类似Base::func1的东西基本上只意味着它遵循vtable指针,然后在指定的偏移量处使用指向函数的指针(例如,第四个函数指针)。

至少现在,我将忽略多重继承。它增加了相当复杂的图片(特别是当涉及到虚拟继承时),你根本没有提到它,所以我怀疑你真的很在乎。

至于访问任何的这种,或者使用比简单地调用虚函数以外的任何方式:你可以拿出一些特定的编译器 - 但不要指望它是便携式的。虽然像调试器这样的东西经常需要看这样的东西,但涉及的代码往往非常脆弱并且特定于编译器。

+0

我认为你的图片与Derived :: vtable是误导性的,因为它看起来像你需要查看func2的'Base :: vtable',但是(如你的文本所描述的)'func1'和'func2 '必须内嵌存储在'Derived :: vtable'中。此外,图片可能表明'Derived :: vtable'中的'func1' /'func2'仍然可以指向'A :: *'(如果Derived没有覆盖它)。 – Stefan

-1

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

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

基地的

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

D1的

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

D2的

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

此外衍生DROM D1或D2将只是添加在2电流下面的列表中的新的虚拟功能。

当调用虚函数,我们只需要调用相应的索引,功能1将是指数0

所以您的通话

dPtr->function1(); 

实际上是

dPtr->_vptr[0](); 
+0

你能指出标准中指定这一点的部分吗? –

4

虚拟表应该在一个类的实例之间共享。更确切地说,它生活在“阶级”层面,而不是实例层面。每个实例都具有实际具有指向虚拟表的指针的开销,如果其层次结构中存在虚函数和类。

表格本身至少是容纳每个虚函数指针所需的大小。除此之外,它是一个实现细节,它是如何定义的。查询here以获取更多关于此问题的SO问题。

1

答案杰里·科芬了优异的解释虚函数指针的工作如何实现运行时多态性在C++中。但是,我认为它缺乏回答存储虚拟表的内存的地方。正如其他人指出的那样,这不是由标准决定的。

然而,由马丁Kysel是进入非常详细哪里虚表存储一个极好的blog post(s)。总结博客文章:

  1. 一个虚拟表是创建为每个类(不是实例)与虚拟功能。
  2. 每个虚函数表存储在此类指向同一V表中存储的每个实例只读
  3. 为vtable中的每个函数的拆卸被存储在所得到的ELF二进制文件的文本部分中的产生的二进制文件的存储器
  4. 试图写在虚函数表,位于只读存储器,导致分段故障(如预期)
3

首先,下面的答案包含几乎所有的东西你想关于虚拟表就知道: https://stackoverflow.com/a/16097013/8908931

如果你正在寻找的东西多一点具体的(与常规的免责声明,这可能的平台,编译器和CPU架构之间变化):

  1. 当需要时,正在为一个类创建一个虚拟表。该类将只有一个虚拟表的实例,并且该类的每个对象都有一个指向该虚拟表的内存位置的指针。虚拟表本身可以被认为是一个简单的指针数组。
  2. 当您将派生指针分配给基指针时,它还包含指向虚表的指针。这意味着基指针指向派生类的虚表。编译器会将此调用指向虚拟表中的偏移量,该虚拟表将包含派生类中函数的实际地址。
  3. 不是。通常在对象开始时,有一个指向虚拟表本身的指针。但这不会对你有太大的帮助,因为它只是一系列指针,并没有真正指示它的大小。
  4. 制作一个很长的短的答案:对于一个确切大小,你可以找到在可执行该信息(或者从它加载到内存段)。有了足够的关于虚拟表的工作原理的知识,只要知道代码,编译器和目标架构,就可以得到相当准确的估计。

    对于确切的大小,就可以找到此信息在任一可执行文件,或在正被从可执行加载在内存段。可执行文件通常是一个ELF文件,这种文件包含运行程序所需的信息。这些信息的一部分是各种语言结构的符号,如变量,函数和虚拟表。对于每个符号,它都包含它在内存中占用的大小。所以按钮行,您将需要虚拟表的符号名称和ELF中的足够知识,以便提取您想要的内容。