目录

一、栈展开(Stack Unwinding)是什么?

二、析构函数抛异常的危险

三、异常安全的三个级别

级别1:不抛异常保证(nothrow)

级别2:强保证(strong guarantee)

级别3:基本保证(basic guarantee)

四、异常安全级别对比

五、完整例子:实现强保证

六、栈展开的实际影响

1. 智能指针在栈展开中自动释放资源

2. 构造函数中抛异常,已构造的成员会被析构

七、常见错误

1. 析构函数中抛异常

2. 认为基本保证足够所有场景

3. 忘记处理异常导致资源泄漏

八、这一篇的收获


一、栈展开(Stack Unwinding)是什么?

当一个异常被抛出时,程序会从 throw 点向上查找匹配的 catch。在这个过程中,所有局部对象会被自动析构(按构造的逆序),就像正常离开作用域一样。

cpp

#include <iostream>
#include <stdexcept>
using namespace std;

class Resource {
    string name;
public:
    Resource(const string& n) : name(n) {
        cout << "构造: " << name << endl;
    }
    ~Resource() {
        cout << "析构: " << name << endl;
    }
};

void funcC() {
    Resource r3("C3");
    throw runtime_error("错误发生");
    Resource r4("C4");  // 永远不会构造
}

void funcB() {
    Resource r2("B2");
    funcC();
    Resource r5("B5");  // 永远不会构造
}

void funcA() {
    Resource r1("A1");
    funcB();
}

int main() {
    try {
        funcA();
    }
    catch (const exception& e) {
        cout << "捕获异常: " << e.what() << endl;
    }
    return 0;
}

输出:

text

构造: A1
构造: B2
构造: C3
析构: C3
析构: B2
析构: A1
捕获异常: 错误发生

关键观察

  1. 对象按构造逆序析构(C3 → B2 → A1)

  2. throw 之后的代码(如 C4B5)永远不会执行

  3. 只有已经完成构造的对象才会被析构


二、析构函数抛异常的危险

如果在栈展开过程中,某个对象的析构函数又抛出异常,就会同时存在两个异常,C++ 会立即调用 std::terminate(),程序崩溃。

cpp

class Dangerous {
public:
    ~Dangerous() {
        throw runtime_error("析构时异常");   // ❌ 极其危险
    }
};

void risky() {
    Dangerous d;
    throw runtime_error("原始异常");
}

int main() {
    try {
        risky();
    }
    catch (...) {
        cout << "捕获异常" << endl;  // 永远不会执行
    }
    return 0;
}

运行结果:程序直接崩溃(terminate)。

结论析构函数永远不应该抛出异常。如果析构函数中的操作可能失败,应该:

  • 在内部 catch 所有异常并记录日志

  • 或者重新设计,把可能失败的操作移到普通成员函数中

cpp

class Safe {
    FILE* f;
public:
    ~Safe() {
        try {
            if (f) fclose(f);
        }
        catch (...) {
            // 记录日志,但不传播异常
        }
    }
};

三、异常安全的三个级别

异常安全保证描述一个函数(或操作)在抛出异常时,对程序状态的影响程度。

级别1:不抛异常保证(nothrow)

函数保证永远不会抛出异常。析构函数和 swap 函数通常应该满足这个级别。

cpp

void swap(MyClass& a, MyClass& b) noexcept {
    // 只做指针交换,不会抛异常
    std::swap(a.ptr, b.ptr);
}

语法noexcept 关键字明确声明。

级别2:强保证(strong guarantee)

操作要么完全成功,要么完全失败,失败时程序状态和操作前完全相同(原子性)。

cpp

class Vector {
    int* data;
    size_t size, capacity;
public:
    void push_back(const int& value) {
        if (size >= capacity) {
            // 先分配新内存(可能抛异常)
            int* new_data = new int[capacity * 2];
            // 复制旧数据(可能抛异常)
            std::copy(data, data + size, new_data);
            // 到这里都没有抛异常,才替换
            delete[] data;
            data = new_data;
            capacity *= 2;
        }
        data[size++] = value;
    }
};

如果 new 或 copy 抛异常,原对象保持不变——强保证。

级别3:基本保证(basic guarantee)

操作失败后,程序处于有效状态(无资源泄漏、不变量保持),但具体内容可能已改变。

cpp

void assign(const string& new_value) {
    // 可能修改了状态才抛异常
    // 但不会泄漏资源,对象仍可正常使用
    delete[] data;
    data = new char[new_value.size()];  // 如果这里抛异常,data 已变成空指针
    strcpy(data, new_value.c_str());
}

上面代码只满足基本保证(不泄漏,对象仍有效),但不满足强保证(状态已改变)。


四、异常安全级别对比

级别 保证 例子 适用场景
不抛异常 绝不抛异常 析构函数、swap 栈展开期间、事务提交
强保证 成功或回滚 vector::push_back(有时) 数据库操作、事务
基本保证 无泄漏,有效状态 大多数函数 日常代码最低要求
无保证 可能泄漏或无效 旧代码 应该避免

实践建议

  • 析构函数:不抛异常保证

  • 普通函数:至少基本保证

  • 关键操作:尽量提供强保证

  • 不满足任何保证的代码应该重构


五、完整例子:实现强保证

cpp

#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;

class String {
private:
    char* data;
    size_t len;
    
    // 交换函数(不抛异常)
    void swap(String& other) noexcept {
        std::swap(data, other.data);
        std::swap(len, other.len);
    }
    
public:
    String(const char* s = "") {
        len = strlen(s);
        data = new char[len + 1];
        strcpy(data, s);
    }
    
    // 拷贝构造(可能抛异常)
    String(const String& other) : len(other.len) {
        data = new char[len + 1];   // 可能抛异常
        strcpy(data, other.data);
    }
    
    // 移动构造(不抛异常)
    String(String&& other) noexcept : data(other.data), len(other.len) {
        other.data = nullptr;
        other.len = 0;
    }
    
    // 析构函数(不抛异常)
    ~String() {
        delete[] data;
    }
    
    // 赋值操作符(强保证——使用 copy-and-swap)
    String& operator=(const String& other) {
        if (this != &other) {
            String temp(other);   // 拷贝构造(可能抛异常)
            swap(temp);           // 不抛异常
        }
        return *this;
    }
    
    // 移动赋值(不抛异常)
    String& operator=(String&& other) noexcept {
        if (this != &other) {
            delete[] data;
            data = other.data;
            len = other.len;
            other.data = nullptr;
            other.len = 0;
        }
        return *this;
    }
    
    const char* c_str() const { return data ? data : ""; }
    
    void print() const { cout << "String: " << c_str() << endl; }
};

int main() {
    String s1("hello");
    String s2("world");
    
    cout << "赋值前: ";
    s1.print();
    s2.print();
    
    s1 = s2;   // 强保证:如果拷贝失败,s1 不受影响
    
    cout << "赋值后: ";
    s1.print();
    s2.print();
    
    return 0;
}

关键技巧 copy-and-swap

  1. 先创建临时副本(可能失败,但原对象不变)

  2. 临时副本成功创建后,用 swap 交换(不抛异常)

  3. 临时副本销毁时释放原资源

这样赋值操作就满足了强异常安全保证


六、栈展开的实际影响

1. 智能指针在栈展开中自动释放资源

cpp

void func() {
    unique_ptr<int> p(new int(42));
    throw runtime_error("error");
    // p 会被自动析构,内存释放
}

2. 构造函数中抛异常,已构造的成员会被析构

cpp

class Member {
public:
    Member() { cout << "Member 构造" << endl; }
    ~Member() { cout << "Member 析构" << endl; }
};

class Container {
    Member m1;
    Member m2;
public:
    Container() : m1(), m2() {
        throw runtime_error("构造失败");
    }
};

int main() {
    try {
        Container c;
    }
    catch (...) {
        cout << "捕获异常" << endl;
    }
}

输出:

text

Member 构造
Member 构造
Member 析构
Member 析构
捕获异常

已构造的 m1 和 m2 被正确析构。


七、常见错误

1. 析构函数中抛异常

cpp

~Resource() {
    if (close() == -1) {
        throw runtime_error("close failed");   // ❌ 程序可能崩溃
    }
}

2. 认为基本保证足够所有场景

某些场景(如数据库事务、文件操作)必须要求强保证。

3. 忘记处理异常导致资源泄漏

cpp

void bad() {
    int* p = new int[1000];
    // 如果这里抛异常,p 不会 delete
    delete[] p;
}

用智能指针或 RAII 类解决。


八、这一篇的收获

你现在应该理解:

  • 栈展开:异常抛出后,从 throw 到 catch 之间的局部对象按构造逆序自动析构

  • 析构函数不能抛异常:栈展开期间再次抛异常会导致 terminate

  • 异常安全三级

    • 不抛异常:析构函数、swap

    • 强保证:成功或回滚(如 copy-and-swap)

    • 基本保证:无泄漏,状态有效(最低要求)

  • copy-and-swap 技巧:实现赋值操作的强保证

💡 小作业:写一个 Transaction 类,在构造函数中开始事务,析构函数中如果未提交则自动回滚(不抛异常)。提供 commit() 方法(可能失败抛异常)。写代码演示:正常提交、异常回滚、栈展开时的自动回滚。


下一篇预告:第35篇《构造函数与异常:如何避免资源泄露?》——构造函数抛异常时,已完成构造的成员对象会被析构,但动态分配的资源(用裸指针)不会自动释放。如何用智能指针或 try-catch 避免泄露?下篇详解。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐