什么时候为对象设置虚拟表指针(在C ++中)?

When exactly does the virtual table pointer (in C++) gets set for an object?

我知道,对于任何具有虚函数的类或从具有虚函数的类派生的类,编译器都会做两件事。首先,它为该类创建一个虚拟表,其次,它在对象的基础部分放置一个虚拟指针(vptr)。在运行时,这个vptr被分配,并在对象被实例化时开始指向正确的vtable。

我的问题是,在实例化过程中,这个vptr是在哪里设置的?vptr的分配是否发生在构造函数之前/之后的对象的构造函数内?


这完全依赖于实现。

对于大多数编译器,

编译器在每个构造函数的成员初始值设定项列表中初始化这个->vptr。

其思想是使每个对象的v指针指向其类的v-table,编译器为此生成隐藏代码并将其添加到构造函数代码中。类似:

1
2
3
4
5
Base::Base(...arbitrary params...)
   : __vptr(&Base::__vtable[0])  ← supplied by the compiler, hidden from the programmer
 {

 }

这个C++ FAQ解释了到底发生了什么。


指向vtable的指针在层次结构中每个构造函数的条目上更新,然后在每个析构函数的条目上再次更新。vptr将开始指向基类,然后随着不同级别的初始化而更新。

虽然您将从许多不同的人那里了解到这是实现定义的,因为它是vtables的全部选择,但事实是所有编译器都使用vtables,并且一旦您选择vtable方法,标准确实规定运行时对象的类型是正在执行的构造函数/析构函数的类型,而这反过来意味着t不管动态调度机制是什么,它都必须随着构建/破坏链的遍历而调整。

请考虑以下代码段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

struct base;
void callback( base const & b );
struct base {
   base() { callback( *this ); }
   ~base() { callback( *this ); }
   virtual void f() const { std::cout <<"base" << std::endl; }
};
struct derived : base {
   derived() { callback( *this ); }
   ~derived() { callback( *this ); }
   virtual void f() const { std::cout <<"derived" << std::endl; }
};
void callback( base const & b ) {
   b.f();
}
int main() {
   derived d;
}

标准规定,该程序的输出为basederivedderivedbase,但callback中的调用与函数的所有四个调用相同。唯一可以实现它的方法是在构建/销毁过程中更新对象中的vptr。


这篇msdn的文章在Great Detali中解释了这一点。

上面写着:

"And the final answer is... as you'd expect. It happens in the constructor."

如果我可以在构造函数的开头添加,在您的构造函数中可能拥有的任何其他代码被执行之前。

但是要小心,假设您有一个类A,一个从A派生的类A1。

  • 如果您创建一个新的对象,vptr将设置在类的构造函数的开头。
  • 但如果创建新对象A1:

"Here's the entire sequence of events when you construct an instance of class A1:

  • A1::A1 calls A::A
  • A::A sets vtable to A's vtable
  • A::A executes and returns
  • A1::A1 sets vtable to A1's vtable
  • A1::A1 executes and returns"

  • 在构造函数的主体中,可以调用虚拟函数,因此,如果实现使用了vptr,则该vptr已经设置。

    请注意,在ctor中调用的虚拟函数是在该构造函数的类中定义的,而不是可能被更派生的类重写的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    #include <iostream>

    struct A
    {
        A() { foo (); }
        virtual void foo () { std::cout <<"A::foo" << std::endl; }
    };

    struct B : public A
    {
        virtual void foo () { std::cout <<"B::foo" << std::endl; }
    };


    int
    main ()
    {
        B b;      // prints"A::foo"
        b.foo (); // prints"B::foo"
        return 0;
    }


    虽然它是依赖于实现的,但它实际上必须在构造函数本身的主体被评估之前发生,因为您可以按照C++规范(12.7/3)允许通过构造函数主体中的EDCOX1×0指针来访问非静态类方法。因此,在调用construtor主体之前必须设置vtable,否则通过this指针调用虚拟类方法将无法正常工作。尽管EDCOX1、0指针和VTebe是两种不同的东西,但是C++标准允许EDCOX1×0指针在构造器的主体中使用这一事实说明编译器必须如何实现符合标准的EDCOX1×0指针正确使用的VTABLE,至少从时序角度来看是正确的。e.如果在调用构造函数主体期间或之后初始化vtable,那么使用this指针调用构造函数主体内的虚拟函数或将this指针传递给依赖动态调度的函数将是有问题的,并且会产生未定义的行为。