虚函数调用机制

Virtual function calling mechanism

本文关键字:机制 函数调用      更新时间:2023-10-16
class base {
public:
  virtual void fn(){}
};

class der: public base {
public:
  void fn(){}
};
der d;
base *b = &d;
b->fn();

当编译器遇到语句b->fn()时,编译器可以获得以下信息:

  1. b是指向类基的指针,
  2. 基类具有虚函数和vptr。

我的问题是:class der的vptr是如何在运行时出现的?

神圣标准不需要vptr或vptr表。然而,在实践中,这是唯一的实现方式。

所以这里’s伪代码发生了什么:

  1. a_base_compatible_vtable_ptr = b->__vtable_ptr__
  2. a_func_ptr = a_base_compatible_vtable_ptr[INDEX_FOR_fn]
  3. a_func_ptr( b )

一个主要的见解是,对于der类的对象,对象中的虚函数表指针将指向der类’与base类兼容;vtable,但包含指向der类的指针。函数实现。

因此,调用函数的der实现。

实际上,在(3)点传递的this指针参数通常是经过优化的,特殊的,通过将this指针传递到专用处理器寄存器中,而不是在机器堆栈中。

有关更深入的讨论,请参阅有关c++内存模型的文献,例如stanley lippman的著作Inside the c++ Object model

干杯,hth。,

在推理这一点时,它有助于我保持类的内存布局的清晰图像,特别是der对象包含 base子对象,该子对象具有与任何其他base对象完全相同的内存布局。

特别是你的base对象布局将简单地包含一个指向vtable的指针(没有字段),derbase子对象也将包含该指针,只有存储在指针中的值不同,它将引用base vtable的der版本(为了使其更有趣,考虑baseder都包含成员):

// Base object         // base vtable (ignoring type info)
+-------------+        +-----------+
| base::vptr  |------> | &base::fn |
+-------------+        +-----------+
| base fields |
+-------------+
// Derived object      // der vtable
+-------------+        +-----------+
| base::vptr  |------> | &der::fn  |
+-------------+        +-----------+ 
| base fields |
+-------------+ <----- [ base subobject ends here ]
| der fields  |
+-------------+

如果你看这两张图,你可以在der对象中识别base子对象,当你做base *bp = &d;时,你正在做的是在 der中获得一个指向base子对象的指针。在这种情况下,base子对象的内存位置与base子对象的内存位置完全相同,但不必如此。重要的是,指针将指向base子对象,并且指向的内存具有base的内存布局,但不同的是,存储在对象中的指针将指向der版本的vtable。

当编译器看到代码bp->fn()时,会认为它是一个base对象,并且知道vptrbase对象中的位置,也知道fn是虚表中的第一项,所以只需要生成bp->vptr[ 0 ]()的代码。如果bp指向base对象,那么bp->vptr指向base 虚表bp->vptr[0]指向base::fn。另一方面,如果指针指向der对象,则bp->vptr指向der虚表,bp->vptr[0]指向der::fn

请注意,在编译时,这两种情况生成的代码完全相同:bp->vptr[0](),并且它根据存储在base(子)对象中的数据(特别是存储在vptr中的值)被分派给不同的函数,该值在构造过程中被更新。

通过清楚地关注base子对象必须存在并且base对象兼容的事实,您可以考虑更复杂的场景,如多重继承:

struct data { 
   int x;
};
class other : public data, public base {
   int y;
public:
   virtual void fn() {}
};
+-------------+
| data::x     |
+-------------+ <----- [ base subobject starts here ] 
| base::vptr  |
+-------------+
| base fields |
+-------------+ <----- [ base subobject ends here ]
| other::y    |
+-------------+
int main() {
   other o;
   base *bp = o;
}

这是一个更有趣的情况,这里有另一个基数,此时调用base * bp = o;创建了一个指向base子对象的指针,并且可以验证指向与o对象不同的位置(尝试打印出&obp的值)。从调用站点来看,这并不重要,因为bp具有静态类型base*,编译器总是可以解引用该指针来定位base::vptr,使用该指针在虚函数表中定位fn,并最终调用other::fn

在这个例子中有更多的魔力,因为otherbase子对象没有对齐,在调用实际的函数other::fn之前,必须调整this指针。编译器通过不在other 虚表中存储指向other::fn的指针来解决这个问题,而是在虚表中存储指向的指针(一小段代码固定了this的值并将调用转发给other::fn)

在典型的实现中,每个对象只有一个vptr。如果对象的类型是der,指针将指向der虚表,而如果对象的类型是base,指针则指向base虚表。该指针在构造时设置。类似这样:

class base {
public:
  base() {
    vptr = &vtable_base;
  }
  virtual void fn(){}
protected:
   vtable* vptr;
};

class der: public base {
public:
  der() {
    vptr = &vtable_der;
  }
  void fn(){}
};

b->fn()的调用做了类似的事情:

vtable* vptr = b->vptr;
void (*fn_ptr)() = vtpr[fn_index];
fn_ptr(b);

我在这里找到了最好的答案。

Vptr是对象的属性,而不是指针的属性。因此,b (base)的静态类型无关紧要。重要的是它的动态类型(der)。b所指向的对象的vptr指向der的虚方法表(vtbl)。

当你调用b->fn()时,它是der的虚拟方法表,它被咨询以确定要调用哪个方法。

vptr不是"类的",而是"实例化对象"的。当构造d时,首先为它分配空间(也包含vptr)。

然后构造base (vptr指向基虚表),然后围绕base构造der,并更新vptr指向der虚表。

baseder虚表都有fn()函数的表项,由于vptr指向前者,当调用b->fn()时,实际上会调用vptr(p)[fn_index]()

但是由于vptr(p) == vptr(&d),在这种情况下,对der::fn的调用将返回