










提到C++中的栈展开,我们有必要先了解一下C++中的资源管理模式
RAII --- Resource Acquisition is initialization 资源获取即初始化, 是C++中管理资源生命周期的重要方式. 它是C++语言的一种管理资源、避免资源泄漏的惯用法,利用栈对象自动销毁的特点来实现,因此,我们可以通过构造函数获取资源,通过析构函数释放资源。
核心点: 将资源的获取和释放与对象的生命周期紧密绑定
核心思想
资源获取即初始化(RAII): 在对象的构造函数中完成资源的分配或获取操作,保证资源在对象创建时就处于可用状态
资源释放即清理: 在对象的析构函数中进行资源的释放操作,保证资源在对象生命周期结束时被正确回收,避免引起内存泄漏
作用域控制生命周期:利用对象的作用域来自动管理资源的生命周期,当对象离开其作用域时,析构函数自动调用,从而释放内存资源
好了,了解了RAII,我们现在来看C++异常处理机制中的栈展开 stack unwinding => 栈展开的目的是负责在C++抛出异常时,能够正确地释放资源
我们知道,栈是一种后进先出(LIFO)的数据结构. 在C++中,当调用一个函数时,会在栈上创建一个栈帧,用于存储函数的局部变量和其他信息。当函数返回时,其栈帧会被销毁
但是,在这个过程中,如果函数执行时抛出一个异常,控制流就会立即跳到处理该异常的代码,而不会正常返回. => 这也就是说,函数的栈帧可能没有被正确销毁,从而导致资源泄露. => 为了解决这个问题,C++中引入了栈展开stack unwinding机制 => 它的执行逻辑基本是这样的: 当一个异常被抛出并且没有在当前作用域内被捕获时,程序会去寻找能够处理该异常的捕获块(catch block). 在这个过程中,程序会依次退出当前作用域,并调用每个作用域中对象的析构函数,以确保资源被正确释放 => 这一整个过程就叫做 栈展开 stack unwinding
栈展开stack unwinding的详细步骤
std::terminate,导致程序非正常终止。我们来看一个例子
#include <iostream> #include <stdexcept> class Book { public: Book(const std::string& name) : name(name) { std::cout << "Acquire resource for Book: " << name << std::endl; } ~Book() { std::cout << "Release resource for Book: " << name << std::endl; } private: std::string name; } void FunctionC() { Book book("C"); throw std::runtime_error("An error occurred in FunctionC"); } void FunctionB() { Book book("B"); FunctionC(); } void FunctionA() { Book book("A"); FunctionB(); } int main() { try { FunctionA(); } catch(const std::exception& e) { std::cerr << "Caught exception: " << e.what() << std::endl; } return 0; }
以上代码的输出为
Acquiring resource for Book: A Acquiring resource for Book: B Acquiring resource for Book: C Releasing resource for Book: C Releasing resource for Book: B Releasing resource for Book: A Caught exception: Exception in functionC
functionA、functionB 和 functionC 分别分配了资源 "A"、"B" 和 "C"。functionC 抛出了一个 std::runtime_error 异常。functionC 的作用域结束,资源 "C" 被释放。functionB 的作用域结束,资源 "B" 被释放。functionA 的作用域结束,资源 "A" 被释放。main 函数中的 catch 块捕获了异常,并输出错误信息好了,栈展开stack unwinding基本就说明白了,现在我们来看看注意事项
注意事项
std::terminate,导致程序非正常终止。因此,析构函数应该被声明为 noexcept,确保它们不会抛出异常。好了,现在我们来看一道经典的C++题目
class A { ~A() {throw exception;} } try { A a;
a.method(); //throw exception } catch(...) {}
上面代码会有问题么 => 100%有问题,上面代码是C++中一个经典的坑 => 析构函数抛异常 + 正在处理另一个异常 → 程序直接终止
这段代码的执行流程如下
执行流程如下:
a.method() 抛出异常catch)a 开始析构~A() 又抛出异常 ❌❌std::terminate()catch(...) 根本不会执行那么,这段代码应该怎么修改呢,我们来看一个修改版本
#include <iostream> #include <exception> class A { public: void method() { throw std::runtime_error("error in method"); } ~A() noexcept // 明确声明不抛异常 { try { cleanup(); // 把可能出错的逻辑放这里 } catch (const std::exception& e) { std::cerr << "Destructor caught exception: " << e.what() << std::endl; } catch (...) { std::cerr << "Destructor caught unknown exception" << std::endl; } } private: void cleanup() { // 模拟可能出错的逻辑 throw std::runtime_error("error in cleanup"); } }; int main() { try { A a; a.method(); } catch (const std::exception& e) { std::cout << "Caught: " << e.what() << std::endl; } }
这个版本改动点
👉 原则是:
析构函数可以“失败”,但不能“抛出去”
也就是说
析构函数内部“可以发生异常”,但绝不能让异常逃出析构函数(escape)
也就是说:
throwcatch 并处理掉cleanup() 抛异常 ✅catch 捕获 ✅此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。