%% # 纲要 > 主干纲要、Hint/线索/路标 # Q&A #### 已明确 #### 待明确 > 当下仍存有的疑惑 **❓<font color="#c0504d"> 有什么问题?</font>** %% # 异常处理机制 异常处理机制为程序**运行时**的 **异常检测** 和 **异常处理** 这两方面的协作提供支持,提供了一种将**错误处理代码**和**程序正常逻辑**进行**分离**的处理方式。 C++的异常处理包括**三个部分**: - **`throw` 表达式** :异常检测部分通过该表达式来**抛出(raise)异常**。 - `try-catch` 异常处理 - 一个 **`try` 语句块**(try block):该语句块中抛出的异常将被捕获,并被某个 catch 子句处理。 - 一个或多个 **`catch` 子句**(catch clause):异常处理代码,处理异常 - 一套**异常类**(exception class):用于在 `throw` 表达式和相关的 `catch` 子句之间传递异常的具体信息 > [!info] "**异常安全**" 程序——**在异常发生期间==正确执行了 "清理" 工作==的程序**被称作为 "**异常安全**" 代码。 <br><br> ## 异常处理代码搜索过程 当异常被抛出时,被抛出的异常对象会被**传递到==调用栈==中逐层回退地寻找匹配的 `catch` 块进行处理** [^1] : - 首先搜索**抛出该异常的函数**,若没有匹配的 `catch` 子句,则**终止该函数**并在**调用该函数的上层函数中**继续搜索; - 以此类推,直到找到**匹配的 `catch` 子句**,则**把==程序的控制权==转移给==处理该异常的代码**==; - 如果异常**传播到了 `main` 函数之外**仍未被捕获,程序**将调用 ==`std::terminate`==,默认会终止当前程序(非正常退出)**。 上述**搜寻 `catch` 时==依次退出函数==** 的过程称之为 "**==栈展开==**" (stack unwinding), 在此过程中,当退出某个块作用域或函数时,**编译器将负责确保其中的局部对象能够被正确销毁**。 <br><br> ## `throw` 表达式 `throw` 表达式用于抛出异常,关键字后跟着**要抛出的==异常对象==**,**表达式的类型**即为**抛出的==异常类型==**。 `throw` 表达式对 "**异常对象**" 进行 "**拷贝初始化**"。 ```cpp #include <iostream> #include <stdexcept> void MyFunc(bool error) { if (error) { throw std::runtime_error("An error occurred"); } // 正常执行的代码 // ... } int main() { try { MyFunc(true); } catch (std::runtime_error& e) { std::cerr << "Caught an exception: " << e.what() << std::endl; } } ``` <br> ### 重新抛出 在 **`catch` 语句**或 **`catch` 语句直接或间接调用的函数**中,可使用 **==空的 `throw` 语句==**:`throw;`, 代表 "**==重新抛出==**"——**将其==捕获的异常对象==沿着调用链向上传递**。 > [!caution] `throw;` 重新抛出的是 "**==原始异常对象==**",故仅当 `catch` 参数是 "**引用类型**" 时,**`catch` 中对异常对象的修改**才会被保留 ```cpp try { ... } catch (my_error& eObj) { // 引用类型 eObj.status = errCodes::severeErr; // 修改了异常对象, 重新抛出时会保留修改. throw; // 重新抛出的是"原异常对象" } catch (other_error eObj) { // 非引用, 而是得到原异常对象的副本 eObj.status = errCodes::badErr; throw; // 这里重新抛出的是"原异常对象" } ``` <br><br> ## `try-catch` 语句 `catch` 子句中的异常声明决定了 "**所能捕获的异常类型**",约束如下[^2]。 - `catch` 语句**按照==声明顺序==逐一进行匹配**。故越具有 "**专门性**" 的异常类应放在靠前位置,例如**派生类异常类型应当放在其基类之前**。 - `catch` 语句中声明的 "**异常类型**" 参数,可以是: - **==值类型==**(将**拷贝得到异常对象的副本**) - **==左值引用==**,不能是右值引用; - **`...` 省略号**,表示 "**==捕获所有异常==**"。 - `catch` 语句中支持的**类型转换**只有三种,除此之外的**标准转换、用户自定义转换均不支持**。 - **非常量 => 常量**的转换; - **派生类向基类或基类引用的转换** - **数组名 => 指向数组元素类型的指针** & **函数名 => 函数指针**的转换; ```cpp try { // 程序正常执行逻辑 // ... } catch (exception-declaration) { // 异常声明 // 异常处理逻辑... } catch (exception-declaration) { // 异常声明 // 异常处理逻辑... } catch (...) { // 异常声明中为省略号"..."时, 表示"捕获所有异常" // 异常处理逻辑... } ``` #### 捕获构造函数 "成员初始化列表" 中抛出的异常 在构造函数的 "**成员初始化列表的冒号之前**" 声明 `try` 关键字,并**在函数体后紧随 `catch` 子句**,称之为 "**==函数 try 语句块==**"。 这一声明将能同时捕获 "**==成员初始化列表中==**" 以及 "**==构造函数体内==**" 抛出的异常。 ```cpp template <typename T> class Foo { public: // try 位于 "成员初始化列表的冒号之前". // 将能捕获 "成员初始化列表" 以及 "构造函数体" 中抛出的异常. Foo(std::initializer_list<T> li) try : data(std::make_shared<std::vector<T>>(li)) { // ... } catch (const std::bad_alloc& e) { handle_out_of_memory(e); } private: std::shared_ptr<std::vector<T>> data; }; ``` > [!NOTE] catch 子句中支持 "**多态**"。 > > 当 `catch` 参数是一个**基类类型或其引用**时,可以用一个**派生类类型的异常对象**进行初始化。 > 若声明的异常类型为 **==基类引用==**,而实际捕获到 "**派生类实例**" 时,可以在 catch 子句中实现 "**多态**" 行为。 > > ```cpp > class Base { > public: > virtual void show() const { > cout << "Base class exception" << endl; > } > > virtual ~Base() = default; > }; > > class Derived : public Base { > public: > void show() const { > cout << "Derived class exception" << endl; > } > }; > > int main() { > try { > throw Derived(); // 抛出派生类对象 > } catch (Base& e) { > e.show(); // 多态:: 调用派生类的虚函数版本. > } > } > ``` > > <br><br><br> # 标准异常 > ![[_attachment/02-开发笔记/01-cpp/异常处理相关/异常处理.assets/IMG-异常处理-0019FF59A0AC3A882494C6DD565C2E0B.png|700]] > [!info] 上图中的标准异常类的定义在多个不同的头文件中: > > ![[_attachment/02-开发笔记/01-cpp/异常处理相关/异常处理.assets/IMG-异常处理-17CC25895E5CBE296491A30152AEBCBA.png|600]] > C++标准库提供了一系列**标准异常类**,定义在 `<stdexcept>` 等头文件中。 所有被 **C++语言本身**或**标准库**抛出的**异常类**,都派生自 **`std::exception` 基类**(定义于 `<exception` >头文件) 。 这些标准异常类可以划分成三组: - **语言本身支持的异常**(Language support) - 例如 `std::bad_alloc`(当`new`操作失败时抛出) - **逻辑错误**(Logic error),`std::logic_error` - 例如 `std::invalid_argument` - **运行时错误**(Runtime error),`std::runtime_error` - 例如 `std::overflow_error` 开发者也可以定义自己的异常类型,通常通过**继承`std::exception`类或其派生类**来实现, 确保自定义异常类型包含 `what()`成员函数,以返回异常描述。 > [!info] > - "**运行时错误**" 是只有在**程序运行时才能检测到的错误**。 > - "**逻辑错误**" 是源自程序代码编写上的逻辑错误。 > [!info] 标准异常类的说明 > > - 基类 `exception` 仅定义了拷贝构造函数、拷贝赋值运算符、虚析构函数、**==`.what()` 虚函数==**; > - 类 `std::runtime_error` 与 `std::logic_error` 无默认构造函数,**只有一个可接受 C 风格字符串或 `std::string` 的构造函数**。 > - 类 `std::bad_cast`、`std::bad_alloc` 等**具有默认构造函数**。 > > > [!quote] > C++标准库是异构的。其中不同的部分具有不同的设计和实现风格。**错误和异常处理**是体现差异的典型例子。 > > - 库的某些部分,**如 string 类,支持详细的错误处理,检查可能发生的每一个问题,并在出现错误时抛出异常**。 > - 其他部分,如 STL 和值数组,更追求速度而不是安全性,因此它们**很少检查逻辑错误**,**只有在发生运行时错误时才抛出异常**。 <br> ## 异常类的信息 标准异常类都提供了 **`what()` 成员函数**,某些异常类还会提供 `code()` 成员函数。 - `what()` 返回一个**以 `\0` 结尾的字符数组**,其内容是 "**用于初始化异常对象**" 的信息。 - 该 C-string 在异常对象被摧毁后,或是当该异常对象被赋予新值后,就不再有效了 ```cpp namespace std {    class exception {    public:        virtual const char* what() const noexcept;       ...   }; } ``` <br><br><br> # 差错码差错状态 - **==差错码==**(Error Code):差错码是一种**轻型对象**,用来封装**差错码值**(error code value)。 - **差错码值**: 通常编译器实现指定 (implementation-specific),也有某些差错码值是标准化的。 - **==差错状态==**(Error Conditions):提供了 “**对错误描述的可移植抽象**” 的对象 对于异常,**C++标准库具体给出的有时是差错码,有时是差错状态**,取决于上下文和环境。 - Class `std::errc` 提供针对 `std::system_error` 异常而定义的**差错状态**。这些异常对应于`<cerrno>`或`<errno.h>` 中定义的标准系统错误号。 - Class `std::io_errc` 提供针对 `std::ios_base::failure` 异常而定义的**差错码**,这种异常由 `stream class` 抛出(since C++11); - Class `std::future_errc` 提供针对 `std::future_error` 异常的**差错码**,这种异常由并发库(concurrency library) 抛出。 %% ## 处理差错码、差错状态 %% <br><br> # Buffer ## 闪念 > sudden idea ## 候选资料 > Read it later # ♾️参考资料 # Footnotes [^1]: 《C++ Primer》P175,P684 [^2]: 《C++ Primer》P175,P687