20 / 80 · C++11 · 约 10 分钟
构造与析构顺序 Construction order
最派生类先初始化虚基类,再按声明顺序初始化直接基类和成员,最后执行构造函数体。初始化列表的书写顺序不能改变规则;析构反向清理已完成的子对象,构造失败也依赖这一确定顺序。
顺序由类定义决定
非委托构造首先初始化虚基类,顺序由继承图深度优先、从左到右的遍历决定,而且同一个虚基类只初始化一次。随后按基类列表顺序初始化直接非虚基类,再按类内声明顺序初始化非静态数据成员,最后执行构造函数体。
把某个成员写在初始化列表最前面,并不会让它抢先构造。若 first_ 的初始化依赖 second_,就必须让 second_ 在类内先声明,并确保其初始化完成;否则看似合理的列表顺序仍可能读取尚未初始化的值。
虚基类由最派生类负责
菱形继承中 Left 与 Right 都虚继承 V,完整对象 D 只包含一个共享的 V 子对象。创建 D 时,由 D 的构造函数提供 V 的初始化参数;Left 和 Right 为 V 写的初始化项在这次构造中被忽略,相关表达式也不会求值。
但单独构造 Left 时,Left 就是最派生类,其 V 初始化项重新生效。初始化列表应按实际执行顺序书写,既帮助读者判断依赖,也避免编译器重排警告。委托构造则先完整执行目标构造函数,再执行委托构造函数体。
逆序清理与构造失败
正常销毁 D 时,先执行 D 析构函数体,再反向销毁成员、直接非虚基类,最后由最派生类负责销毁虚基类。示例用固定容量日志记录这条路径,不依赖地址、对象大小或其他 ABI 细节,也不在析构过程中申请内存。
若普通非委托构造中途抛出异常,尚未构造完成的完整对象不会调用自己的析构函数,但已完成构造的成员和基类会逆序销毁。因此每份资源都应尽早进入 RAII 成员;只保存在裸指针中的资源,不会因为外围构造失败而自动释放。
容易答错的地方
- 在构造函数体里给成员赋值不等于初始化;进入函数体前成员的初始化阶段已经结束。
- 虚继承决定共享基类子对象,不表示普通成员函数自动成为 virtual。
运行一个例子
最低标准 C++11 · 完整程序 · 下载 .cpp
#include <array>
#include <cassert>
#include <cstddef>
struct Log {
std::array<int, 12> items{};
std::size_t used = 0;
void add(int n) noexcept { items[used++] = n; }
};
struct V {
Log& log;
V(Log& l, int id) : log(l) { log.add(id); }
~V() { log.add(-1); }
};
struct Left : virtual V {
explicit Left(Log& l) : V(l, 99) { log.add(2); }
~Left() { log.add(-2); }
};
struct Right : virtual V {
explicit Right(Log& l) : V(l, 98) { log.add(3); }
~Right() { log.add(-3); }
};
struct Member {
Log& log;
int id;
Member(Log& l, int n) : log(l), id(n) { log.add(id); }
~Member() { log.add(-id); }
};
struct D : Left, Right {
Member first, second;
explicit D(Log& l)
: V(l, 1), Left(l), Right(l), first(l, 4), second(l, 5) {
log.add(6);
}
~D() { log.add(-6); }
};
int main() {
Log log;
{ D object(log); }
const std::array<int, 12> expected{{1, 2, 3, 4, 5, 6, -6, -5, -4, -3, -2, -1}};
assert(log.used == expected.size());
assert(log.items == expected);
}
在本地编译
g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread objects-construction.cpp -o example && ./example预期结果
预期:正常退出、无输出;所有 assert 通过。
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
若 second 的构造函数在完成初始化前抛出异常,哪些 D 相关析构函数会执行?
查看参考答案
D 的析构函数体不会执行,second 自身的析构函数也不会执行,因为它没有构造完成。已完成的 first、Right、Left、共享虚基类 V 依次销毁。若 second 内部已经有构造完成的成员,它们仍会由 second 的构造失败清理流程销毁。
继续查证
标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。