C++ 异常安全
外观
C++异常安全
异常安全(Exception Safety)是C++编程中一个关键的设计概念,它确保程序在抛出异常时仍能保持正确的状态,避免资源泄漏或数据损坏。异常安全是编写健壮、可靠代码的重要组成部分。
异常安全的基本概念
在C++中,异常(Exception)是一种处理运行时错误的机制。当函数执行过程中发生错误(如内存不足、无效输入等),可以通过抛出异常来中断当前流程,并由调用栈中最近的异常处理代码捕获。然而,异常可能导致程序状态不一致,例如:
- 资源(内存、文件句柄、锁等)未被释放
- 数据结构部分修改,处于无效状态
- 对象构造或析构不完全
异常安全的目标是确保即使发生异常,程序也能维持以下保证:
异常安全保证级别
C++通常定义三种异常安全保证级别:
- 基本保证(Basic Guarantee):如果异常被抛出,程序处于有效状态,无资源泄漏,但对象的具体状态可能不确定。
- 强保证(Strong Guarantee):如果异常被抛出,程序状态与调用操作前完全相同(原子性操作)。
- 不抛异常保证(No-throw Guarantee):操作保证不会抛出任何异常(如析构函数、移动操作等)。
实现异常安全的技术
资源获取即初始化(RAII)
RAII(Resource Acquisition Is Initialization)是C++管理资源的核心理念,通过对象的生命周期控制资源:
#include <memory>
#include <fstream>
void processFile() {
// 使用智能指针管理内存(异常安全)
auto ptr = std::make_unique<int>(42);
// 使用RAII管理文件句柄
std::ifstream file("data.txt");
if (!file) {
throw std::runtime_error("无法打开文件");
}
// 操作资源...
// 即使此处抛出异常,文件也会自动关闭,内存自动释放
}
拷贝并交换(Copy-and-Swap)
实现强异常保证的常见技术:
class String {
char* data;
size_t length;
public:
// 强异常安全的赋值操作
String& operator=(const String& other) {
String temp(other); // 可能抛出异常(但不影响this)
swap(temp); // 不抛异常操作
return *this;
}
void swap(String& other) noexcept {
std::swap(data, other.data);
std::swap(length, other.length);
}
};
事务性操作
将多个操作组合成原子性事务:
void transferMoney(BankAccount& from, BankAccount& to, double amount) {
// 创建事务保存旧状态
auto oldFrom = from.getBalance();
auto oldTo = to.getBalance();
try {
from.withdraw(amount); // 可能抛出异常
to.deposit(amount); // 可能抛出异常
} catch (...) {
// 回滚操作
from.setBalance(oldFrom);
to.setBalance(oldTo);
throw;
}
}
实际案例
STL容器中的异常安全
标准库容器通常提供基本异常保证,某些操作提供强保证:
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {1, 2, 3};
try {
// push_back提供强异常保证(C++11后)
v.push_back(4);
// 可能抛出异常的插入操作
v.insert(v.begin(), 5);
} catch (const std::bad_alloc& e) {
// 内存不足时,vector保持原有状态
std::cerr << "内存分配失败: " << e.what() << "\n";
}
// 即使异常发生,v仍处于有效状态
for (int num : v) {
std::cout << num << " ";
}
}
构造函数中的异常安全
构造函数需要特别注意异常安全,因为构造失败时析构函数不会被调用:
class ResourceHolder {
int* resource1;
FILE* resource2;
public:
ResourceHolder() : resource1(new int(42)), resource2(fopen("file.txt", "r")) {
if (!resource2) {
delete resource1; // 必须手动清理
throw std::runtime_error("无法打开文件");
}
}
~ResourceHolder() {
delete resource1;
if (resource2) fclose(resource2);
}
};
更好的实现是使用RAII类成员:
class ResourceHolder {
std::unique_ptr<int> resource1;
std::unique_ptr<FILE, decltype(&fclose)> resource2;
public:
ResourceHolder()
: resource1(std::make_unique<int>(42)),
resource2(fopen("file.txt", "r"), &fclose) {
if (!resource2) {
throw std::runtime_error("无法打开文件");
}
}
// 不需要显式析构函数
};
数学表达
异常安全可以形式化为状态转换的数学表达。设程序状态为,操作为:
- 基本保证:
- 强保证:
- 不抛异常保证:
最佳实践总结
1. 优先使用RAII管理所有资源
2. 为可能失败的操作提供适当的异常安全保证
3. 析构函数、移动操作和swap应标记为noexcept
4. 避免在构造函数中抛出异常,除非构造完全失败
5. 使用智能指针(std::unique_ptr
, std::shared_ptr
)管理动态内存
6. 对于复杂操作,考虑事务性实现
7. 文档中明确标注函数的异常安全保证级别
通过遵循这些原则,可以编写出在异常情况下仍能保持正确性和可靠性的C++代码。