对于某些编译器,如果一个类具有虚函数,则可以使用其对象的第一个字节的地址访问其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存储为第一个条目的情况,这样做有什么好处?这有助于提高性能,还是因为使用&amp; b更容易访问vbtl?
答案 0 :(得分:4)
这是一个实现细节,但实际上很多实现都是这样做的。
它非常有效和方便。假设您需要为给定对象调用虚函数。您有一个指向该对象和虚函数索引的指针。您需要以某种方式找到应该使用该索引和此对象调用哪个函数。好的,您只需访问指针后面的第一个sizeof(void*)
字节并找到vtable所在的位置,然后访问vtable的必要元素以获取函数地址。
你可以存储一个单独的“每个对象的vtable”映射,但如果你决定要将vptr存储在对象中,那么使用第一个字节,而不是最后一个字节或任何其他地方是合乎逻辑的因为使用这种方法,一旦你有一个指向对象的指针就知道在哪里找到vptr,不需要额外的数据。
答案 1 :(得分:2)
虽然这是实施定义,但似乎并不是一个真正的选择。
首先,我们可以看到您必须拥有vptr
或嵌入式vtable
。后者意味着你必须在构造上复制vtable
并且它消耗更多的内存,但是它的优点是避免在每个方法调用上使用一个指针解除引用。根据具体情况,两者可能都有很好的理由 - 大多数实现都选择降低构建时间和总体内存消耗,而不是节省调度时间。
当选择vptr
方法时,我们发现必须保持基类和派生类布局的二进制兼容性。首先,我们可以通过(经常)使用一个vptr
来实现这一点,出于兼容性原因,这个vptr
必须存在于最基本的类中。
当处理简单继承时,在派生到基类之间转换的最直接的方法是保持指针值,这意味着布局必须首先是基类的字段,然后是添加派生类有助于它
现在我们非常接近将vptr
放在第一位的原因。它只需靠近对象的开头,因为它必须存在于对象的最基本部分内。
然后我们将它放在偏移0上的原因可能是它是一个可用于所有类的一致偏移。您无法保证可以在vptr
之前放置任何数据。
将vptr
置于偏移0处也有一些优点。如果您知道要拥有vptr
的对象,则您知道必须查看偏移量0而无需知道对象的类型(超过它具有vptr
)。这可以用于某些调试目的(vtable
通常包含足够的信息来推断实际类型)。特别是这使得typeid
和类似的更简单实现,因为您只需要查看相同的偏移量以通过预定义的偏移量检索type_info
节点 - 这意味着您可以共享{{1的实际代码}}