我知道对于任何具有虚函数的类或从具有虚函数的类派生的类,编译器会做两件事。首先,它为该类创建一个虚拟表,其次,它将一个虚拟指针 (vptr) 放在对象的基础部分。在运行时,这个 vptr 被分配并在对象被实例化时开始指向正确的 vtable。
我的问题是,在实例化过程中,这个 vptr 到底是在哪里设置的?vptr 的这种分配是否发生在构造函数之前/之后的对象的构造函数内部?
我知道对于任何具有虚函数的类或从具有虚函数的类派生的类,编译器会做两件事。首先,它为该类创建一个虚拟表,其次,它将一个虚拟指针 (vptr) 放在对象的基础部分。在运行时,这个 vptr 被分配并在对象被实例化时开始指向正确的 vtable。
我的问题是,在实例化过程中,这个 vptr 到底是在哪里设置的?vptr 的这种分配是否发生在构造函数之前/之后的对象的构造函数内部?
这严格依赖于实现。
对于大多数编译器,
编译器在每个构造函数的成员初始化器列表中初始化 this->__vptr。
这个想法是使每个对象的 v-pointer 指向其类的 v-table,编译器为此生成隐藏代码并将其添加到构造函数代码中。就像是:
Base::Base(...arbitrary params...)
: __vptr(&Base::__vtable[0]) ← supplied by the compiler, hidden from the programmer
{
}
这个C++ FAQ 解释了到底发生了什么。
指向 vtable 的指针在层次结构中每个构造函数的入口处更新,然后在每个析构函数的入口处再次更新。vptr 将开始指向基类,然后随着不同级别的初始化而更新。
虽然你会从许多不同的人那里读到这是实现定义的,因为它是 vtables 的全部选择,但事实是所有编译器都使用 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。
那里说:
“最终的答案是……正如你所料。它发生在构造函数中。”
如果我可以添加,就在构造函数的开头,在你的构造函数中可能拥有的任何其他代码被执行之前。
但要小心,假设你有一个类 A,以及一个从 A 派生的类 A1。
“当你构造一个 A1 类的实例时,这是整个事件序列:
- A1::A1 调用 A::A
- A::A 将 vtable 设置为 A 的 vtable
- A::A 执行并返回
- A1::A1 将 vtable 设置为 A1 的 vtable
- A1::A1 执行并返回 "
在构造函数的主体中,可能会调用虚函数,因此,如果实现使用了 a 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;
}
this
虽然它依赖于实现,但它实际上必须在评估构造函数本身之前发生,因为根据 C++ 规范(12.7/3),您可以通过构造函数 body 中的指针访问非静态类方法。 .. 因此,必须在调用构造函数的主体之前设置 vtable,否则通过this
指针调用虚拟类方法将无法正常工作。尽管this
指针和 vtable 是两个不同的东西,但 C++ 标准允许在构造函数的主体中使用指针这一事实this
证明了编译器必须如何实现 vtable 以符合标准的使用this
至少从时序的角度来看,指针可以正常工作。如果在调用构造函数体期间或之后初始化了 vtable,那么使用this
指针调用构造函数体内部的虚函数或将this
指针传递给依赖于动态调度的函数将是有问题的并且会产生未定义的行为。