THIS IS THE TEXT by Morgan Deters and NOT CC-licensed.
Morgan Deters的网页:
多重继承中的构造/析构:
在多重继承的情况下如何在内存中构造上述对象?我们如何确保部分构造的对象(以及其虚表)对构造函数是安全的?
幸运的是,这一切都已经被非常小心地处理了。假设我们正在构造一个新的类型为D
的对象(例如通过new D
)。首先,在堆中分配对象的内存并返回指针。D
的构造函数被调用,但在进行任何D
-特定的构造之前,它会在对象上调用A
的构造函数(当然,在调整this
指针后!)。A
的构造函数填充了D
对象的A
部分,就好像它是A
的实例一样。
d --> +----------+
| |
+----------+
| |
+----------+
| |
+----------+
| | +-----------------------+
+----------+ | 0 (top_offset) |
| | +-----------------------+
+----------+ | ptr to typeinfo for A |
| vtable |-----> +-----------------------+
+----------+ | A::v() |
| a | +-----------------------+
+----------+
控制权返回给 D
的构造函数,该函数调用了 B
的构造函数。(这里不需要进行指针调整)当 B
的构造函数完成时,对象将如下所示:
B-in-D
+-----------------------+
| 20 (vbase_offset) |
+-----------------------+
| 0 (top_offset) |
+-----------------------+
d --> +----------+ | ptr to typeinfo for B |
| vtable |------> +-----------------------+
+----------+ | B::w() |
| b | +-----------------------+
+----------+ | 0 (vbase_offset) |
| | +-----------------------+
+----------+ | -20 (top_offset) |
| | +-----------------------+
+----------+ | ptr to typeinfo for B |
| | +--> +-----------------------+
+----------+ | | A::v() |
| vtable |---+ +-----------------------+
+----------+
| a |
+----------+
等等……B
的构造函数通过更改对象的vtable指针来修改了对象的A
部分!它怎么知道要区分这种B-in-D和B-in-something-else(或单独的B
)呢?简单。 虚表表告诉它要这样做。这个结构,缩写为VTT,是用于构建的虚表的表格。在我们的例子中,D
的VTT看起来像这样:
B-in-D
+-----------------------+
| 20 (vbase_offset) |
VTT for D +-----------------------+
+-------------------+ | 0 (top_offset) |
| vtable for D |-------------+ +-----------------------+
+-------------------+ | | ptr to typeinfo for B |
| vtable for B-in-D |-------------|----------> +-----------------------+
+-------------------+ | | B::w() |
| vtable for B-in-D |-------------|--------+ +-----------------------+
+-------------------+ | | | 0 (vbase_offset) |
| vtable for C-in-D |-------------|-----+ | +-----------------------+
+-------------------+ | | | | -20 (top_offset) |
| vtable for C-in-D |-------------|--+ | | +-----------------------+
+-------------------+ | | | | | ptr to typeinfo for B |
| vtable for D |----------+ | | | +-> +-----------------------+
+-------------------+ | | | | | A::v() |
| vtable for D |-------+ | | | | +-----------------------+
+-------------------+ | | | | |
| | | | | C-in-D
| | | | | +-----------------------+
| | | | | | 12 (vbase_offset) |
| | | | | +-----------------------+
| | | | | | 0 (top_offset) |
| | | | | +-----------------------+
| | | | | | ptr to typeinfo for C |
| | | | +----> +-----------------------+
| | | | | C::x() |
| | | | +-----------------------+
| | | | | 0 (vbase_offset) |
| | | | +-----------------------+
| | | | | -12 (top_offset) |
| | | | +-----------------------+
| | | | | ptr to typeinfo for C |
| | | +-------> +-----------------------+
| | | | A::v() |
| | | +-----------------------+
| | |
| | | D
| | | +-----------------------+
| | | | 20 (vbase_offset) |
| | | +-----------------------+
| | | | 0 (top_offset) |
| | | +-----------------------+
| | | | ptr to typeinfo for D |
| | +----------> +-----------------------+
| | | B::w() |
| | +-----------------------+
| | | D::y() |
| | +-----------------------+
| | | 12 (vbase_offset) |
| | +-----------------------+
| | | -8 (top_offset) |
| | +-----------------------+
| | | ptr to typeinfo for D |
+----------------> +-----------------------+
| | C::x() |
| +-----------------------+
| | 0 (vbase_offset) |
| +-----------------------+
| | -20 (top_offset) |
| +-----------------------+
| | ptr to typeinfo for D |
+-------------> +-----------------------+
| A::v() |
+-----------------------+
D的构造函数将一个指向D的VTT的指针传递给B的构造函数(在这种情况下,它传递了第一个B-in-D条目的地址)。事实上,上面用于对象布局的虚表是一个特殊的虚表,仅用于B-in-D的构造。
控制权返回到D的构造函数,并调用C的构造函数(带有一个指向“C-in-D + 12”条目的VTT地址参数)。当C的构造函数完成对象时,它看起来像这样:
B-in-D
+-----------------------+
| 20 (vbase_offset) |
+-----------------------+
| 0 (top_offset) |
+-----------------------+
| ptr to typeinfo for B |
+---------------------------------> +-----------------------+
| | B::w() |
| +-----------------------+
| C-in-D | 0 (vbase_offset) |
| +-----------------------+ +-----------------------+
d --> +----------+ | | 12 (vbase_offset) | | -20 (top_offset) |
| vtable |--+ +-----------------------+ +-----------------------+
+----------+ | 0 (top_offset) | | ptr to typeinfo for B |
| b | +-----------------------+ +-----------------------+
+----------+ | ptr to typeinfo for C | | A::v() |
| vtable |--------> +-----------------------+ +-----------------------+
+----------+ | C::x() |
| c | +-----------------------+
+----------+ | 0 (vbase_offset) |
| | +-----------------------+
+----------+ | -12 (top_offset) |
| vtable |--+ +-----------------------+
+----------+ | | ptr to typeinfo for C |
| a | +-----> +-----------------------+
+----------+ | A::v() |
+-----------------------+
从上面您可以看到,C的构造函数再次修改了嵌入式A的虚表指针。现在,嵌入式的C和A对象正在使用特殊构造的C-in-D虚表,而嵌入式的B对象则使用特殊构造的B-in-D虚表。最后,D的构造函数完成了这项工作,我们得到了与之前相同的图示:
+-----------------------+
| 20 (vbase_offset) |
+-----------------------+
| 0 (top_offset) |
+-----------------------+
| ptr to typeinfo for D |
+----------> +-----------------------+
d --> +----------+ | | B::w() |
| vtable |----+ +-----------------------+
+----------+ | D::y() |
| b | +-----------------------+
+----------+ | 12 (vbase_offset) |
| vtable |---------+ +-----------------------+
+----------+ | | -8 (top_offset) |
| c | | +-----------------------+
+----------+ | | ptr to typeinfo for D |
| d | +-----> +-----------------------+
+----------+ | C::x() |
| vtable |----+ +-----------------------+
+----------+ | | 0 (vbase_offset) |
| a | | +-----------------------+
+----------+ | | -20 (top_offset) |
| +-----------------------+
| | ptr to typeinfo for D |
+----------> +-----------------------+
| A::v() |
+-----------------------+
销毁发生的过程与构造相反。D 的析构函数被调用。在用户的销毁代码运行之后,析构函数调用 C 的析构函数,并指示其使用 D 的 VTT 的相关部分。C 的析构函数以与构造期间相同的方式操作 vtable 指针;也就是说,相关的 vtable 指针现在指向 C-in-D 构造期间的 vtable。然后它运行了 C 的用户销毁代码并返回控制权到 D 的析构函数,接下来 D 的析构函数通过 D 的 VTT 引用调用 B 的析构函数。B 的析构函数设置了对象的相关部分,使其引用 B-in-D 构造期间的 vtable。它运行了 B 的用户销毁代码并返回控制权到 D 的析构函数,最后 D 的析构函数调用 A 的析构函数。A 的析构函数将 A 部分对象的 vtable 更改为指向 A 的 vtable。最后,控制权返回到 D 的析构函数,对象的销毁完成。曾经由该对象使用的内存已经返回给系统。
实际上,这个故事有些更加复杂。你是否看到过 GCC 警告和错误信息中的 “in-charge” 和 “not-in-charge” 构造函数和析构函数规范,或者在 GCC 生成的二进制文件中看到过?事实上,可能会有两个构造函数实现和最多三个析构函数实现。
"in-charge"(或完整对象)构造函数是构造虚基类的构造函数,而 "not-in-charge"(或基类对象)构造函数则是不构造虚基类的构造函数。考虑上面的例子。如果构造一个 B,它的构造函数需要调用 A 的构造函数来构造它。同样,C 的构造函数需要构造 A。但是,如果 B 和 C 在构造 D 时构造,则它们的构造函数不应构造 A,因为 A 是虚基类,D 的构造函数将为 D 的实例精确地构造一次。考虑以下情况:
如果你执行 new A,A 的 "in-charge" 构造函数将被调用来构造 A。
当你执行 new B,B 的 "in-charge" 构造函数将被调用。它将调用 A 的 "not-in-charge" 构造函数。
new C 与 new B 类似。
一个新的 D 调用 D 的 "in-charge" 构造函数。我们通过这个例子进行了讲解。D 的 "in-charge" 构造函数调用了 A、B 和 C 的 "not-in-charge" 构造函数(按顺序)。
"in-charge" 析构函数是 "in-charge" 构造函数的类比——它负责销毁虚基类。同样,会生成一个 "not-in-charge" 析构函数。但是还有第三个。 "in-charge deleting" 析构函数是一种同时释放存储和销毁对象的析构函数。那么何时优先调用其中之一呢?
好吧,有两种可以被销毁的对象——在栈上分配的对象和在堆上分配的对象。考虑以下代码(给出我们之前的菱形继承层次结构,使用虚继承):
D d;
D *pd = new D;
delete pd;
return;
我们可以看到,执行删除的代码并没有调用实际的delete操作符,而是由被删除对象的负责删除的析构函数来完成的。为什么要这样做?为什么不让调用者调用负责删除的析构函数,然后再删除对象呢?这样你只需要两个析构函数的实现而不是三个...
好吧,编译器确实可以这样做,但出于其他原因会更加复杂。考虑以下代码(假设有一个虚析构函数,您总是使用它,对吧?...对吧!?):
D *pd = new D; // allocates a D in the heap and constructs it
C *pc = d; // we have a pointer-to-C that points to our heap-allocated D
/* ... */
delete pc; // call destructor thunk through vtable, but what about delete?
如果您没有一个“负责删除”的D析构函数,那么delete操作将需要像析构函数的thunk一样调整指针。请记住,C对象嵌入在D中,因此我们上面的指向C的指针被调整为指向D对象的中间位置。我们不能只是删除这个指针,因为它不是在我们构造它时由malloc()
返回的指针。
因此,如果我们没有一个负责删除的析构函数,我们就必须有一个到delete操作符的thunks(并在我们的vtable中表示它们),或者类似的其他东西。
Thunk, 虚拟和非虚拟
本部分尚未编写。
带有一侧虚拟方法的多重继承
好了。最后一个练习。如果我们有一个具有虚拟继承的菱形继承层次结构,如前所述,但仅沿着其中一侧具有虚拟方法怎么办?所以:
class A {
public:
int a;
};
class B : public virtual A {
public:
int b;
virtual void w();
};
class C : public virtual A {
public:
int c;
};
class D : public B, public C {
public:
int d;
virtual void y();
};
在这种情况下,对象的布局如下:
+-----------------------+
| 20 (vbase_offset) |
+-----------------------+
| 0 (top_offset) |
+-----------------------+
| ptr to typeinfo for D |
+----------> +-----------------------+
d --> +----------+ | | B::w() |
| vtable |----+ +-----------------------+
+----------+ | D::y() |
| b | +-----------------------+
+----------+ | 12 (vbase_offset) |
| vtable |---------+ +-----------------------+
+----------+ | | -8 (top_offset) |
| c | | +-----------------------+
+----------+ | | ptr to typeinfo for D |
| d | +-----> +-----------------------+
+----------+
| a |
+----------+
因此,您可以看到C子对象没有虚拟方法,但仍具有vtable(尽管为空)。实际上,所有C的实例都有一个空的vtable。
感谢Morgan Deters!