为啥 vptr 存储为具有虚函数的类的内存中的第一个条目?

Posted

技术标签:

【中文标题】为啥 vptr 存储为具有虚函数的类的内存中的第一个条目?【英文标题】:Why is vptr stored as the first entry in the memory of a class with virtual functions?为什么 vptr 存储为具有虚函数的类的内存中的第一个条目? 【发布时间】:2016-02-07 05:24:53 【问题描述】:

对于某些编译器,如果一个类有虚函数,那么它的 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 更容易访问 vbtl?

【问题讨论】:

“对象的第一个字节访问的vptr”的行为是否因操作系统而异? 这将取决于实现。 “我们知道,如果一个类有虚函数,那么它的 vptr 可以通过其对象的第一个字节的地址来访问”。不,我们没有。 反例:GCC 的早期版本(我认为是 3.2 之前的版本)将 vptr 存储在类的末尾(除非它从基类继承了 vptr)。原因是这样,对象的开头与具有相同字段的 C 结构具有相同的布局。 【参考方案1】:

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

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

When chosen vptr approach we see that we must keep binary compatibility of the layout of base and derived classes.首先,我们可以通过(经常)使用一个vptr 来实现这一点,这个vptr 出于兼容性原因必须存在于最基本的类中。

在处理简单继承时,在派生类和基类之间进行转换的最直接方式是保留指针值,这意味着布局必须首先是基类的字段,然后是派生类的附加贡献它。

现在我们非常接近将vptr 放在首位的原因。它必须位于对象的开头附近,因为它必须存在于对象的最基本部分中。

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

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

【讨论】:

【参考方案2】:

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

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

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

【讨论】:

以上是关于为啥 vptr 存储为具有虚函数的类的内存中的第一个条目?的主要内容,如果未能解决你的问题,请参考以下文章

自己的真实面试

C++ 构造函数不能是虚函数,基类析构函数应该为虚函数

c++ 之 内存模型:虚函数篇

C++中虚函数实现原理揭秘

空类,含有虚函数的类的大小

对象模型