多重继承下的构造/销毁
上面的对象在构造对象本身的时候是如何在内存中构造的呢?我们如何确保构造函数可以安全地操作一个部分构造的对象(及其 vtable)?
幸运的是,这一切都为我们精心处理。假设我们正在构造一个类型的新对象D
(例如,通过new D
)。首先,在堆中分配对象的内存并返回一个指针。D
的构造函数被调用,但在执行任何D
特定的构造之前,它调用A
对象上的构造函数(当然是在调整this
指针之后!)。A
的构造函数填充对象的A
一部分,D
就好像它是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
的构造A
函数通过改变它的 vtable 指针来修改对象的一部分!它是如何知道将这种 B-in-D 与 B-in-something-else(或B
就此而言的独立)区分开来的?简单的。虚拟表表告诉它这样做。这种结构,缩写为VTT,是用于构造的 vtable 表。在我们的例子中,VTTD
看起来像这样:
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 条目的地址)。而且,实际上,用于上述对象布局的 vtable 是一个特殊的 vtable,仅用于构建 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 的 vtable 指针。嵌入的 C 和 A 对象现在使用特殊构造 C-in-D vtable,而嵌入 B 对象使用特殊构造 B-in-D vtable。最后,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 的析构函数,它接下来调用 B 的析构函数并引用 D 的 VTT。B 的析构函数设置对象的相关部分以引用 B-in-D 构造 vtable。它运行用户对 B 的销毁代码并将控制权返回给 D 的析构函数,最终调用 A 的析构函数。一个' s 析构函数将对象 A 部分的 vtable 更改为引用 A 的 vtable。最后,控制权返回到 D 的析构函数,对象的析构完成。对象曾经使用过的内存返回给系统。
现在,事实上,这个故事有点复杂。您是否曾在 GCC 生成的警告和错误消息或 GCC 生成的二进制文件中看到那些“负责”和“不负责”的构造函数和析构函数规范?好吧,事实是可以有两个构造函数实现和最多三个析构函数实现。
“负责”(或完整对象)构造函数是构造虚拟基础的构造函数,而“不负责”(或基础对象)构造函数则不是。考虑我们上面的例子。如果构造了一个B,它的构造函数需要调用A的构造函数来构造它。类似地,C 的构造函数需要构造 A。但是,如果 B 和 C 是作为 D 构造的一部分构造的,则它们的构造函数不应构造 A,因为 A 是虚拟基,而 D 的构造函数将负责构造它一次以 D 为例。考虑以下情况:
如果你做一个新的A,A的“负责”构造函数被调用来构造A。当你做一个新的B时,B的“负责”构造函数被调用。它将调用 A 的“非负责人”构造函数。
新 C 类似于新 B。
一个新的 D 调用 D 的“负责”构造函数。我们浏览了这个例子。D 的“负责”构造函数调用 A、B 和 C 的构造函数的“非负责”版本(按此顺序)。
“负责”析构函数类似于“负责”构造函数——它负责破坏虚拟基地。类似地,生成了一个“不负责”的析构函数。但还有第三个。“负责删除”析构函数是一种释放存储空间以及销毁对象的析构函数。那么什么时候优先调用另一个呢?
好吧,有两种对象可以被破坏——分配在堆栈上的和分配在堆上的。考虑这段代码(给定我们之前的带有虚拟继承的菱形层次结构):
D d; // allocates a D on the stack and constructs it
D *pd = new D; // allocates a D in the heap and constructs it
/* ... */
delete pd; // calls "in-charge deleting" destructor for D
return; // calls "in-charge" destructor for stack-allocated D
我们看到实际的删除运算符不是由执行删除的代码调用的,而是由负责删除对象的删除析构函数调用的。为什么要这样做?为什么不让调用者调用负责的析构函数,然后删除对象?那么你将只有两个析构函数实现的副本,而不是三个......
好吧,编译器可以做这样的事情,但由于其他原因,它会更复杂。考虑这段代码(假设你总是使用一个虚拟析构函数,对吧?...对吧?!?):
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 的析构函数的“负责删除”种类,那么删除操作将需要像析构函数 thunk 一样调整指针。请记住,C 对象嵌入在 D 中,因此我们上面的指向 C 的指针被调整为指向 D 对象的中间。我们不能只删除这个指针,因为它不是原来的指针malloc()
在我们构建它时返回。
因此,如果我们没有负责的删除析构函数,我们就必须对 delete 运算符(并在我们的 vtable 中表示它们)或其他类似的东西进行 thunk。
Thunks,虚拟和非虚拟
这部分还没写。
一侧具有虚拟方法的多重继承
好的。最后一个练习。如果我们像以前一样有一个带有虚拟继承的菱形继承层次结构,但只有一侧有虚拟方法呢?所以:
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。