【c++面向对象编程】第34篇:栈展开(Stack Unwinding)与异常安全级别
目录一、栈展开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 捕获异常: 错误发生关键观察对象按构造逆序析构C3 → B2 → A1throw之后的代码如C4、B5永远不会执行只有已经完成构造的对象才会被析构二、析构函数抛异常的危险如果在栈展开过程中某个对象的析构函数又抛出异常就会同时存在两个异常C 会立即调用std::terminate()程序崩溃。cppclass Dangerous { public: ~Dangerous() { throw runtime_error(析构时异常); // ❌ 极其危险 } }; void risky() { Dangerous d; throw runtime_error(原始异常); } int main() { try { risky(); } catch (...) { cout 捕获异常 endl; // 永远不会执行 } return 0; }运行结果程序直接崩溃terminate。结论析构函数永远不应该抛出异常。如果析构函数中的操作可能失败应该在内部catch所有异常并记录日志或者重新设计把可能失败的操作移到普通成员函数中cppclass Safe { FILE* f; public: ~Safe() { try { if (f) fclose(f); } catch (...) { // 记录日志但不传播异常 } } };三、异常安全的三个级别异常安全保证描述一个函数或操作在抛出异常时对程序状态的影响程度。级别1不抛异常保证nothrow函数保证永远不会抛出异常。析构函数和swap函数通常应该满足这个级别。cppvoid swap(MyClass a, MyClass b) noexcept { // 只做指针交换不会抛异常 std::swap(a.ptr, b.ptr); }语法noexcept关键字明确声明。级别2强保证strong guarantee操作要么完全成功要么完全失败失败时程序状态和操作前完全相同原子性。cppclass 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操作失败后程序处于有效状态无资源泄漏、不变量保持但具体内容可能已改变。cppvoid 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先创建临时副本可能失败但原对象不变临时副本成功创建后用swap交换不抛异常临时副本销毁时释放原资源这样赋值操作就满足了强异常安全保证。六、栈展开的实际影响1. 智能指针在栈展开中自动释放资源cppvoid func() { unique_ptrint p(new int(42)); throw runtime_error(error); // p 会被自动析构内存释放 }2. 构造函数中抛异常已构造的成员会被析构cppclass 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; } }输出textMember 构造 Member 构造 Member 析构 Member 析构 捕获异常已构造的m1和m2被正确析构。七、常见错误1. 析构函数中抛异常cpp~Resource() { if (close() -1) { throw runtime_error(close failed); // ❌ 程序可能崩溃 } }2. 认为基本保证足够所有场景某些场景如数据库事务、文件操作必须要求强保证。3. 忘记处理异常导致资源泄漏cppvoid 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 避免泄露下篇详解。