为什么vptr被存储为具有虚拟函数的类的内存中的第一个条目

Why is vptr stored as the first entry in the memory of a class with virtual functions?

本文关键字:内存 第一个 函数 虚拟 vptr 存储 为什么      更新时间:2023-10-16

对于某些编译器,如果类具有虚拟函数,则可以使用其对象的第一个字节的地址访问其vptr。例如,

class Base{
public:
    virtual void f(){cout<<"f()"<<endl;};
    virtual void g(){cout<<"g()"<<endl;};
    virtual void h(){cout<<"h()"<<endl;};
};
int main()
{   
   Base b;
   cout<<"Address of vtbl:"<<(int *)(&b)<<endl;
   return 0;
}

我知道它依赖于不同的编译器行为。既然vptr是作为第一个条目存储的,那么这样做的好处是什么?这是否有助于提高性能,或者仅仅是因为使用&b

这是一个实现细节,但实际上许多实现都是这样做的。

它相当高效和方便。假设您需要为给定的对象调用一个虚拟函数。您有一个指向该对象和虚拟函数索引的指针。您需要找到应该使用该索引和该对象调用的函数。好吧,您只需访问指针后面的第一个sizeof(void*)字节,找到vtable所在的位置,然后访问vtable的必要元素以获取函数地址。

您可以为每个对象存储一个单独的"vtable for each object"映射或其他东西,但如果您决定将vptr存储在对象内部,那么使用第一个字节,而不是最后一个字节或任何其他位置是合乎逻辑的,因为使用这种方法,一旦您有了指向对象的指针,就知道在哪里可以找到vptr,而不需要额外的数据。

尽管这是定义的实现,但似乎没有太多真正的选择。

首先,我们可以看到,以太必须有一个vptr或一个嵌入的vtable。后者意味着您将不得不在构造时复制vtable,并且它会消耗更多内存,但其优点是避免在每个方法调用上取消引用一个指针。根据具体情况,这两种方法可能都有很好的论据——大多数实现都选择了降低构建时间和总体内存消耗,而不是节省调度时间。

当选择vptr方法时,我们看到我们必须保持基类和派生类布局的二进制兼容性。首先,我们可以通过(经常)使用一个vptr来实现这一点,出于兼容性的原因,这个vptr必须生活在最基本的类中。

当处理简单继承时,在派生类到基类之间转换的最直接的方法是保持指针值,这意味着布局必须首先是基类的字段,然后是派生类的添加。

现在我们已经很接近为什么把vptr放在第一位了。它只需要靠近对象的起点,因为它必须生活在对象最基本的部分。

那么,我们把它放在偏移量0上的原因可能是它是一个对所有类都可用的一致偏移量。您根本无法保证有任何数据可以放在vptr之前。

将CCD_ 10置于偏移0处也具有一些优点。如果您知道对象具有vptr,那么您就知道您必须查看偏移量0,而不需要知道对象的类型(比它具有vptr更多)。这可以方便地用于某些调试目的(vtable通常包含足够的信息来推断实际类型)。特别是这使得typeid和类似的代码实现起来更简单,因为您只需要查看相同的偏移量就可以通过预定义的偏移量检索type_info节点,这意味着您可以共享typeid的实际代码。