跳转到内容

C++ 异常安全

来自代码酷
Admin留言 | 贡献2025年4月28日 (一) 21:31的版本 (Page update by admin bot)

(差异) ←上一版本 | 已核准修订 (差异) | 最后版本 (差异) | 下一版本→ (差异)

C++异常安全

异常安全(Exception Safety)是C++编程中一个关键的设计概念,它确保程序在抛出异常时仍能保持正确的状态,避免资源泄漏或数据损坏。异常安全是编写健壮、可靠代码的重要组成部分。

异常安全的基本概念

在C++中,异常(Exception)是一种处理运行时错误的机制。当函数执行过程中发生错误(如内存不足、无效输入等),可以通过抛出异常来中断当前流程,并由调用栈中最近的异常处理代码捕获。然而,异常可能导致程序状态不一致,例如:

  • 资源(内存、文件句柄、锁等)未被释放
  • 数据结构部分修改,处于无效状态
  • 对象构造或析构不完全

异常安全的目标是确保即使发生异常,程序也能维持以下保证:

异常安全保证级别

C++通常定义三种异常安全保证级别:

  1. 基本保证(Basic Guarantee):如果异常被抛出,程序处于有效状态,无资源泄漏,但对象的具体状态可能不确定。
  2. 强保证(Strong Guarantee):如果异常被抛出,程序状态与调用操作前完全相同(原子性操作)。
  3. 不抛异常保证(No-throw Guarantee):操作保证不会抛出任何异常(如析构函数、移动操作等)。

graph LR A[异常安全保证] --> B[基本保证] A --> C[强保证] A --> D[不抛异常保证]

实现异常安全的技术

资源获取即初始化(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("无法打开文件");
        }
    }
    // 不需要显式析构函数
};

数学表达

异常安全可以形式化为状态转换的数学表达。设程序状态为S,操作为f:SS

  • 基本保证:sS,要么 f(s) 成功,要么 s 保持有效状态
  • 强保证:sS,要么 f(s) 成功,要么 s=f(s)
  • 不抛异常保证:sS,f(s) 必定成功

最佳实践总结

1. 优先使用RAII管理所有资源 2. 为可能失败的操作提供适当的异常安全保证 3. 析构函数、移动操作和swap应标记为noexcept 4. 避免在构造函数中抛出异常,除非构造完全失败 5. 使用智能指针(std::unique_ptr, std::shared_ptr)管理动态内存 6. 对于复杂操作,考虑事务性实现 7. 文档中明确标注函数的异常安全保证级别

通过遵循这些原则,可以编写出在异常情况下仍能保持正确性和可靠性的C++代码。