C++中的虚表指针是在什么时候为一个对象设置的?

9
我知道对于任何具有虚函数或派生自具有虚函数的类,编译器会做两件事。首先,它为该类创建虚表;其次,在对象的基本部分中放置一个虚指针(vptr)。在运行时,当对象被实例化时,vptr被分配并开始指向正确的虚表。
我的问题是,在实例化过程中到底在什么地方设置了这个vptr?这个vptr的赋值是在对象的构造函数内部还是之前/之后发生的?

1
这完全取决于实现的方式。 - Cat Plus Plus
7
@CatPlusPlus 又来了...任何询问虚函数表的问题都是依赖于实现的。没必要重申显而易见的事实。 - David Rodríguez - dribeas
从技术上讲,在构造函数执行之前必须设置指针,因为此时对象已经是该类型的对象(尽管未初始化)。但是,标准根本不需要vtable,这只是实现虚拟函数的常见方式。此外,由于在构造函数中静态地知道了类型,因此从构造函数中调用的虚拟函数是静态解析的。因此,事实上可以在之后初始化vtable(即使它是错误的,您也不会注意到)。 - Damon
3
@Damon:其实你可以将*this传递给一个以你的基础类型引用为参数的函数,并从那里调用虚函数。在那个函数内部进行的分配必须是动态的,因为编译器不知道调用者是谁。 - David Rodríguez - dribeas
1
@Als:我猜这取决于你想要一个理论的计算机科学方法还是一个实践的方法。我不知道除了vtable和vptr之外是否有其他选择被实现到C++中,而且标准所规定的行为决定了vptr必须如何更新(如果这是动态分派的解决方案)。是的,它没有被标准强制执行,但同时在实现上是几乎独立的,因为所有实现在这方面都是相同的。 - David Rodríguez - dribeas
显示剩余4条评论
5个回答

10

这是严格依赖于实现的。

对于大多数编译器而言,

编译器在每个构造函数的成员初始化列表中初始化 this->__vptr

其目的是使每个对象的虚指针指向其类的虚函数表,并且编译器会生成隐藏代码并将其添加到构造函数代码中。大致如下:

Base::Base(...arbitrary params...)
   : __vptr(&Base::__vtable[0])  ← supplied by the compiler, hidden from the programmer
 {
   
 }

这篇 C++ FAQ解释了虚函数的基本概念。


2
这只取决于使用vtable的选择,因此实现有所不同。对于所有使用vtable的编译器(即所有),在每个构造/析构级别上更新vptr是不可避免的。 - David Rodríguez - dribeas
@DavidRodríguez-dribeas:我同意,我所知道的所有编译器都使用vtablevptr机制,并且应该使用这样的机制,但由于实现不受标准约束,因此出现了第一个声明。 - Alok Save
@DavidRodríguez-dribeas 我不怪他加上免责声明 - 如果你不这样做,太多人会绕过你的答案甚至点踩它。 - Mark Ransom
我还要补充一点,值得注意的是,在执行对象的构造函数时,虚指针被设置,如上面的答案所述,而虚表本身是由编译器在编译阶段创建的。 - Guy Avraham

9

每个构造函数的入口和每个析构函数的入口都会更新指向虚函数表的指针。vptr将开始指向基类,随着不同层次的初始化而更新。

虽然您可能会从许多不同的人那里读到这是实现定义的,因为它是整个vtable选择的问题,但事实是所有编译器都使用vtables,一旦您选择了vtable方法,则标准确实规定了运行时对象的类型是正在执行的构造函数/析构函数,反过来意味着无论动态调度机制是什么,在遍历构建/销毁链时都必须进行调整。

考虑以下代码片段:

#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;
}

标准规定该程序的输出是base, derived, derived, base,但对函数的四次调用中来自callback的调用相同。唯一实现这一点的方法是在对象的构造/析构过程中更新对象中的vptr。

1

这篇MSDN文章详细解释了它

文章中说:

"最终的答案就是...顾名思义,它发生在构造函数中。"

如果我可以补充一下,在构造函数的开始之前,执行任何其他可能存在的构造函数代码。


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

  • 如果创建一个新的A对象,则vptr将在A类的构造函数的开始处设置。
  • 但是,如果创建一个新的A1对象:

"当您构造类A1的实例时,以下是整个事件序列:

  1. A1::A1调用A::A
  2. A::A将vtable设置为A的vtable
  3. A::A执行并返回
  4. A1::A1将vtable设置为A1的vtable
  5. A1::A1执行并返回"

0
在构造函数的主体中,可以调用虚函数,因此,如果实现使用了 vptr,那么该 vptr 已经设置好了。
请注意,在 ctor 中调用的虚函数是在 构造函数类中定义的函数,而不是可能被更派生类覆盖的函数。
#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;
}

根据ISO/IEC 14882:2011(E) [class.cdtor] 12.7 Construction and destruction [#4],“成员函数,包括虚函数(10.3),可以在构造或析构期间被调用。”它们在构造函数中绝对会被调用。 - chill
1
@Pubby8:虽然在这种特定情况下,构造函数可以使用静态分派,但并非所有情况都可以这样做。特别是如果构造函数调用一个调度函数,并传递对 A 的引用,而调度程序调用虚函数,则标准规定必须产生相同的结果,在这种情况下,编译器无法使用静态分派,因为它在处理调度程序时不知道调用来自哪里。 - David Rodríguez - dribeas

0

虽然它是实现相关的,但实际上必须在构造函数本身的主体被评估之前发生,因为根据C++规范(12.7/3),您可以通过构造函数主体中的this指针访问非静态类方法... 因此,在调用构造函数之前必须设置vtable,否则通过this指针调用虚拟类方法将无法正确工作。尽管this指针和vtable是两个不同的东西,但C++标准允许在构造函数主体中使用this指针表明编译器必须如何实现vtable以使符合标准的this指针使用从时间角度正确地工作。如果在构造函数主体期间或之后初始化vtable,则在构造函数主体内使用this指针调用虚拟函数或将this指针传递给依赖于动态分派的函数将会有问题并创建未定义的行为。


1
@Pubby8 ... 我完全同意,但只是想说:1)在构造函数体中,您可以访问非静态类方法;2)如果您通过this指针进行访问,并且vtable未设置,则在调用非静态虚拟函数时会遇到一些问题,因为虚拟函数调用是通过指向this指针的类实例的vtable进行的。 - Jason
@Damon 你上面的评论是错误的。你考虑了一个特殊情况(一个退化的情况),对它做出了正确的陈述,然后将其概括。你关于这些事实是正确的:(1)在构造函数体内,* this 的动态类型根据定义已知 (2) 编译器已知对象的动态类型时,对虚拟调用不必使用vptr。就只有这些。你似乎认为在构造函数中有一个特殊规则与this有关,但实际上并没有。 - curiousguy
@curiousguy:从构造函数中调用的任何虚函数都是通过完全限定名称调用的(在多重继承和具有多个基类中相同名称的函数的情况下),或者可以100%确定它属于哪个基类(名称是唯一的,或者只有一个基类),否则构建将会失败(调用是模糊的)。从技术上讲,甚至不需要this指针,因为对象的静态(而不是动态!)类型根据定义是完全已知的,并且完全已知必须调用哪个函数。这不是特殊情况,而是唯一可能的情况。 - Damon
@Damon,你能帮我回答这些问题吗? - curiousguy
@curiousguy:根据第3.7.4.3段,你可以自己回答所有问题(根据你之前的说法,你应该很熟悉这一段)。 - Damon
显示剩余8条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接