RAII是解决构造函数异常导致资源泄漏的唯一可靠方案:资源必须绑定到对象生命周期,确保“全有或全无”,成员初始化为安全值,获取操作紧邻赋值,避免中途失败遗留裸资源。

构造函数抛异常会导致对象不完整,RAII 是唯一靠谱解法
构造函数里抛异常,对象的析构函数根本不会被调用——因为对象没“活下来”。这不是 bug,是 C++ 标准行为。所以不能靠“在构造函数末尾加 try-catch”来兜底,那只会掩盖资源泄漏。真正能守住资源边界的,只有 RAII(Resource Acquisition Is Initialization):把资源绑定到对象生命周期上,让编译器自动管销毁。
new 表达式失败时,operator new 抛 std::bad_alloc,不是构造函数的问题
很多人以为 new MyClass() 失败是因为构造函数出错,其实分两步:先调 operator new 分配内存,再调构造函数。前者失败抛 std::bad_alloc;后者失败才抛你自定义的异常。这两者处理方式完全不同:
-
operator new异常可被全局set_new_handler拦截,但一般不建议改写——容易引发递归或死锁 - 构造函数异常只能靠外围 try/catch 捕获,且此时对象地址无效,不能 delete,也不能访问任何成员
- 如果用了 placement new,
operator new不抛异常(它不分配内存),构造函数异常就成唯一风险点
RAII 类必须确保“全有或全无”:构造成功则资源已接管,失败则不留下裸指针
写一个 RAII 封装类时,最常踩的坑是:在构造函数里先 new 一块内存,再做其他可能失败的操作(比如打开文件、解析配置),结果中间失败了,裸指针却没被 delete——泄漏就发生了。
正确做法是:所有资源获取操作,必须在“确认能全部成功”之后,才移交所有权。例如:
立即学习“C++免费学习笔记(深入)”;
class FileHandle {
FILE* fp_ = nullptr;
public:
explicit FileHandle(const char* path) {
fp_ = std::fopen(path, "r");
if (!fp_) throw std::runtime_error("cannot open file");
// ✅ fopen 成功后才赋值,失败时 fp_ 仍是 nullptr
}
~FileHandle() { if (fp_) std::fclose(fp_); }
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
};关键点:
- 成员变量初始化为安全值(如
nullptr、-1),避免未初始化状态 - 资源获取操作(
fopen、malloc、pthread_mutex_init)必须紧挨着赋值,中间不插其他可能抛异常的逻辑 - 不要在构造函数里调用虚函数——此时虚表还没完全就绪,行为未定义
std::unique_ptr 和 std::shared_ptr 能简化 RAII,但不能替代构造逻辑设计
用 std::unique_ptr 确实能自动释放堆内存,但它不解决“构造中途失败导致部分资源已分配”的问题。比如:
struct BadExample {
std::unique_ptr<int[]> buf_;
std::unique_ptr<char[]> name_;
BadExample(size_t n) : buf_(new int[n]) { // ✅ buf_ 安全
name_ = std::make_unique<char[]>(n + 1); // ❌ 如果这里 new 失败,buf_ 已分配但无人释放
}
};这代码看似用了智能指针,实际仍泄漏。修复方式只有两种:
- 把所有资源获取集中到构造函数体外,用工厂函数封装(如
make_BadExample()),内部用 try/catch + 手动清理 - 或者改用委派构造:先默认构造,再 move 赋值,保证每一步都原子
- 更推荐的是拆成多个 RAII 类——每个只管一种资源,组合时靠栈顺序销毁(C++ 析构按成员声明逆序执行)
RAII 的难点从来不在语法,而在判断“哪一步才算真正‘拥有’了资源”。这个边界划错了,智能指针也救不了。









