跳转到内容

C++ 异常安全

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

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

C++异常安全

异常安全(Exception Safety)是C++编程中一个关键概念,指程序在抛出异常时仍能保持正确状态,不泄露资源,不破坏数据一致性。异常安全是编写健壮代码的重要原则,尤其在资源管理、并发编程和复杂系统中至关重要。

异常安全的基本保证

C++中的异常安全通常分为三个级别,按强度从低到高排列:

1. 基本保证(Basic Guarantee)

  • 程序在抛出异常后仍处于有效状态,不会发生资源泄漏或内存损坏。
  • 对象仍可被安全销毁,但具体状态可能不确定。

2. 强保证(Strong Guarantee)

  • 操作要么完全成功,要么失败后程序状态回滚到操作前的状态(类似事务)。
  • 通常通过"copy-and-swap"惯用法实现。

3. 不抛保证(No-throw Guarantee)

  • 操作保证不会抛出任何异常。
  • 典型例子:简单getter函数和析构函数(按C++标准,析构函数不应抛出异常)。

pie title 异常安全保证级别 "基本保证" : 40 "强保证" : 35 "不抛保证" : 25

代码示例

基本保证示例

#include <iostream>
#include <stdexcept>

class ResourceHolder {
    int* resource;
public:
    ResourceHolder() : resource(new int(42)) {}
    
    ~ResourceHolder() {
        delete resource; // 确保资源释放
    }
    
    void riskyOperation(bool fail) {
        int* temp = new int(100); // 可能泄漏的资源
        if (fail) {
            delete temp; // 失败时清理
            throw std::runtime_error("Operation failed");
        }
        delete resource; // 释放旧资源
        resource = temp; // 转移所有权
    }
};

解释:即使抛出异常,也没有资源泄漏,满足基本保证。

强保证示例(copy-and-swap惯用法)

#include <algorithm>
#include <stdexcept>

class String {
    char* data;
    size_t length;
    
    void swap(String& other) noexcept {
        std::swap(data, other.data);
        std::swap(length, other.length);
    }
    
public:
    String& operator=(const String& rhs) {
        String temp(rhs); // 所有可能抛出异常的操作在这里完成
        swap(temp);       // 不抛出的交换操作
        return *this;    // temp离开作用域,清理旧资源
    }
    // ... 其他成员函数
};

解释:如果拷贝构造失败,原对象不受影响;只有所有操作成功后才会修改状态。

异常安全实践

RAII(资源获取即初始化)

C++异常安全的核心技术是RAII模式:

#include <memory>
#include <fstream>

void processFile(const std::string& filename) {
    std::ifstream file(filename); // RAII对象,析构时自动关闭文件
    
    if (!file) {
        throw std::runtime_error("无法打开文件");
    }
    
    // 使用文件 - 即使这里抛出异常,文件也会正确关闭
    // ...
} // 文件在这里自动关闭

STL中的异常安全

标准库容器提供不同级别的保证:

  • 向量(vector)的push_back:强保证(除非元素拷贝/移动构造函数抛出)
  • 关联容器的插入操作:通常提供强保证

数学表达

异常安全可以用事务概念形式化表示。设操作前状态为S0,操作后状态为S1

  • 基本保证: 异常e,S有效状态,S未定义
  • 强保证: 异常e,状态=S0或操作完全成功
  • 不抛保证: P(抛出异常)=0

实际案例

数据库事务处理

class DatabaseTransaction {
    Database& db;
public:
    explicit DatabaseTransaction(Database& db) : db(db) {
        db.beginTransaction();
    }
    
    ~DatabaseTransaction() noexcept {
        if (std::uncaught_exceptions()) {
            db.rollback(); // 异常发生时回滚
        } else {
            db.commit();   // 否则提交
        }
    }
    
    // 禁用拷贝
    DatabaseTransaction(const DatabaseTransaction&) = delete;
    DatabaseTransaction& operator=(const DatabaseTransaction&) = delete;
};

void updateAccounts(Account& a, Account& b, int amount) {
    DatabaseTransaction trans(db);
    a.withdraw(amount); // 可能抛出
    b.deposit(amount);  // 可能抛出
    // 如果到达这里,事务自动提交
}

分析: 1. 使用RAII管理事务生命周期 2. 析构函数检测是否在异常上下文中 3. 提供强异常保证:要么全部完成,要么全部回滚

最佳实践

  • 优先使用RAII管理所有资源(内存、文件句柄、锁等)
  • 析构函数必须不抛出异常
  • 对于可能失败的操作,考虑提供强保证
  • 使用noexcept标记不会抛出的函数
  • 在修改对象状态前完成所有可能抛出异常的操作
  • 使用标准库智能指针(std::unique_ptr, std::shared_ptr)管理动态内存

常见陷阱

1. 在构造函数中抛出异常

  - 如果构造函数抛出,析构函数不会被调用
  - 需要在抛出前手动清理已分配的资源

2. 虚函数中的异常规范

  - 派生类重写的虚函数不能添加新的异常类型

3. 异常与多线程

  - 异常不应跨越线程边界传播
  - 使用std::promise/std::future在线程间传递异常

总结

异常安全是C++健壮编程的核心要求。通过理解三种保证级别并应用RAII等技术,可以构建在异常情况下仍能保持正确性的系统。记住:

  • 基本保证是底线
  • 尽可能提供强保证
  • 关键操作(如析构)应提供不抛保证

随着C++标准演进(如C++11引入的noexcept,C++17的std::uncaught_exceptions),编写异常安全代码的工具在不断改进,但基本原则保持不变。