构造函数抛出异常时,仅已成功构造的成员对象和基类子对象会被逆序析构;裸指针、new内存等手动资源不会自动释放,必须用RAII或显式回滚清理。

构造函数抛出异常时,哪些成员会被析构
会析构「已成功完成构造」的成员对象和基类子对象,其余不会。C++ 标准保证栈展开(stack unwinding)按构造逆序调用它们的析构函数;但构造函数体内的裸指针、new 分配的内存、文件句柄等手动资源,**不会自动释放**。
常见错误现象:看到 std::cout << "in C destructor" 输出,就以为所有资源都安全了——其实 resource = new char[1024] 那行之后抛异常,delete[] resource 永远不会执行。
- 基类构造成功 → 其析构函数会被调用
- 成员对象(如
std::string name_、C c)构造成功 → 它们的析构函数会被调用 -
char* raw_ptr或FILE* fp等原始资源 → 不会自动清理,必须手动干预 - 构造函数体中
throw之前的代码已执行 → 后续未执行部分不触发任何清理
为什么不能依赖析构函数清理构造函数里的裸资源
因为对象本身没构造完,它的析构函数根本不会被调用。只有那些「作为子对象独立构造完毕」的成员,才享有被自动析构的权利。
例如:class B { A a_; char* p_; B() : a_() { p_ = new char[100]; throw std::runtime_error(""); } } —— 这里 a_ 的析构函数会运行,但 B 自己的析构函数不会,p_ 就悬在那里。
立即学习“C++免费学习笔记(深入)”;
- 析构函数是对象生命周期的终点,而未完成构造的对象没有“生命周期”
-
noexcept默认适用于析构函数,但它对构造函数无约束力 - 试图在构造函数里
try/catch手动清理,容易漏掉中间步骤或重复释放
正确做法:用 RAII 替代裸资源管理
把资源绑定到局部对象的生命周期上,让栈展开机制替你干活。这是唯一可靠、可维护的解法。
组合式C++代码评审方案,融合静态分析、AI推理、多轮迭代评审和C++专项检查,适用于PR审查、增量代码审查、全项目评审和代码质量评分,触发词包括review cpp、cpp代码评审、C++review、代码审查。
对比示例:
class Bad {
int* p_;
public:
Bad() {
p_ = new int[100];
if (/* failure */) throw std::bad_alloc();
// 如果这里 throw,p_ 泄漏
}
~Bad() { delete[] p_; }
};→ 改成:
class Good {
std::vector<int> data_; // RAII 容器
public:
Good() : data_(100) {
if (/* failure */) throw std::runtime_error("");
// 即使 throw,data_ 析构自动释放内存
}
};- 优先用
std::unique_ptr、std::vector、std::string等标准 RAII 类型 - 自定义资源类(如文件句柄)必须确保其析构函数是
noexcept且不抛异常 - 避免在构造函数里做可能失败的 I/O、网络、数据库连接——这些应拆到独立的
init()或工厂函数中
构造函数里非得分配裸资源?那至少加 noexcept 保护
极少数场景(比如写底层库、兼容旧接口)绕不开 new,此时必须在抛异常前显式释放,且不能依赖后续逻辑。
错误写法:
ResourceHolder() {
p1_ = new A;
p2_ = new B;
if (fail()) throw std::runtime_error("");
}正确写法(最小化风险):
ResourceHolder() {
p1_ = new A;
try {
p2_ = new B;
if (fail()) throw std::runtime_error("");
} catch (...) {
delete p1_;
throw;
}
}- 每一步分配后立即用
try包裹后续操作,形成“分配-使用-失败则回滚”链 - 不能只在函数末尾统一
catch:那样前面已分配的资源无法追溯 - 这种模式易出错、难维护,仅作兜底;上线前务必用 ASan/Valgrind 验证无泄漏
真正棘手的不是“哪些会被析构”,而是“哪些你以为会被析构,其实根本不会”。RAII 不是风格选择,是异常安全的底线。

















