答案:编写异常安全的c++代码需在设计阶段确保资源不泄漏、状态一致,核心是RaiI和异常安全级别。应优先使用智能指针和标准库,采用拷贝再交换实现强烈保证,避免构造函数中复杂操作,析构函数不抛异常,并用noexcept明确接口,通过良好设计防止异常导致的问题。

编写异常安全的C++代码,关键在于确保程序在发生异常时不会造成资源泄漏、数据损坏或状态不一致。异常安全不是事后补救,而是设计阶段就必须考虑的问题。核心目标是:即使抛出异常,程序仍能保持有效状态,并且不丢失资源。
理解异常安全的三个级别
根据异常发生后程序的状态保证程度,异常安全通常分为三个级别:
- 基本保证:如果异常被抛出,对象仍处于“合法”状态,没有资源泄漏,但具体状态不确定。
- 强烈保证:如果异常被抛出,程序状态回滚到调用前的状态(类似事务的原子性)。
- 无异常保证:操作绝对不会抛出异常,通常是内置类型的操作或已知安全的函数。
编写代码时应尽量达到强烈保证,至少满足基本保证。
使用RAII管理资源
RAII(Resource Acquisition Is Initialization)是C++异常安全的基石。它通过对象的构造函数获取资源,析构函数自动释放资源,利用栈展开机制确保资源不泄漏。
立即学习“C++免费学习笔记(深入)”;
常见做法包括:
- 用std::unique_ptr代替裸指针管理动态内存。
- 用std::shared_ptr实现共享所有权。
- 用std::lock_guard或std::unique_lock管理互斥量,避免死锁。
- 封装文件句柄、网络连接等资源为类,析构函数中关闭资源。
只要资源被封装在局部对象中,即使函数中途抛出异常,栈展开会自动调用析构函数,确保资源释放。
拷贝再交换(copy and Swap)技巧
这是实现强烈异常安全的经典方法,尤其适用于赋值操作符。
思路是:先创建一个副本,在副本上修改,成功后再与原对象交换。整个过程要么完成,要么不影响原对象。
示例:
class MyClass {
private:
std::vector<int> data;
public:
MyClass& operator=(MyClass other) {
data.swap(other.data);
return *this;
}
};
参数other通过值传递,自动完成拷贝。如果拷贝过程抛出异常,原对象尚未修改。只有拷贝成功后,才进行交换,交换操作通常不抛出异常(对POD或标准容器而言)。
避免在构造函数中抛出异常时的陷阱
构造函数若未完成,对象被视为未构造成功,其析构函数不会被调用。因此:
- 在构造函数中分配资源时,建议使用智能指针或其他RAII对象持有资源,防止泄漏。
- 不要在构造函数中做复杂操作,尤其是可能抛出异常的IO或网络调用。
- 可考虑使用工厂函数+智能指针返回对象,便于捕获异常并处理。
小心自赋值与异常交互
虽然现代C++中自赋值较少见,但在实现赋值操作时仍需注意。结合异常安全,推荐统一使用“拷贝再交换”,天然避免自赋值问题,同时提供强烈异常安全保证。
总结关键实践
- 优先使用标准库容器和智能指针,它们本身具备良好的异常安全保证。
- 函数设计时,考虑哪些操作可能抛出异常,将其放在修改对象状态之前。
- 修改对象多个成员时,先修改副本,再整体提交(如swap)。
- 确保析构函数绝不抛出异常,否则可能导致程序终止。
- 使用noexcept标注不抛异常的函数,帮助编译器优化并明确接口契约。
基本上就这些。异常安全不是靠临时修补,而是靠良好的设计习惯和对RAII的深刻理解。只要资源管理得当,大部分异常安全问题都能自然化解。


