12 Exception¶
说明
本文档仅涉及部分内容,仅可用于复习重点知识
1 概念¶
在 C++ 中,异常(Exception) 是一种处理程序运行时错误的机制。通过异常处理,可以将错误检测和错误处理分离,提高程序的健壮性和可维护性。C++ 的异常处理主要依赖于 try、throw 和 catch 关键字
C++ 的异常机制是运行时行为,运行时错误(如数组越界、空指针访问、除零错误等)可以通过异常机制捕获,而 编译错误(如语法错误、类型不匹配等)发生在编译阶段,不能通过异常机制捕获
- 异常:程序运行过程中出现的错误或异常情况(如除零、内存分配失败等)
- 抛出(
throw):当检测到异常时,使用throw抛出异常对象 - 捕获(
catch):使用catch捕获并处理异常 - 尝试(
try):用try块包裹可能发生异常的代码
| output | |
|---|---|
注意事项:
- 析构函数抛出异常会导致程序终止,建议用
noexcept - 不建议抛出基本类型(如
int、char*),推荐抛出异常类对象 catch(...)可以捕获所有类型的异常,但无法获取具体信息- 异常处理会有一定的性能开销,建议只在必要时使用
catch(...)真的是三个点哦
单独使用 throw;(不带任何异常对象)只能在 catch 块内部使用,表示“重新抛出当前捕获到的异常”
这样可以将异常继续传递给上层调用者,常用于在捕获异常后进行部分处理,然后让异常继续向上传递
- 按值捕获:
catch (Exception e):会调用拷贝构造函数创建异常对象的副本 - 按引用捕获:
catch (Exception& e):直接捕获原异常对象的引用,无拷贝操作
始终优先选择:catch (const Exception& e),除非有特殊需求。这是 C++ 中高效、安全且支持多态的规范做法
2 assert¶
在 C++ 中,assert 是一个用于 断言 的宏,定义在头文件 <cassert>(或 C 语言的 <assert.h>)中
它的作用是在程序运行时检查某个条件是否为真。如果条件为假,assert 会输出错误信息并终止程序执行,帮助开发者在调试阶段发现程序中的逻辑错误
- 断言的典型用法是:
assert(表达式) - 如果表达式为假,程序会输出类似
Assertion failed: 表达式, file 文件名, line 行号的信息,并终止执行 assert主要用于调试阶段,在发布(Release)版本中可通过定义NDEBUG宏禁用所有断言
3 异常类¶
C++ 标准库 <exception> 提供了一些常用的异常类,如:
std::exception:所有标准异常的基类std::runtime_error、std::logic_error等
可以自定义异常类型(通常继承自 std::exception)
3.1 bad_alloc¶
在 C++ 中,当 new 分配内存失败时,默认不会返回空指针(如旧式 malloc 那样),而是直接抛出 std::bad_alloc 异常
4 异常规范¶
在 C++ 中,异常规范(Exception Specification)用于声明函数可能抛出的异常类型。它主要用于约束和说明函数的异常行为。C++ 异常规范经历了从早期的动态异常规范到现代的 noexcept 关键字的演变
4.1 早期的异常规范(已废弃)¶
- 编译器通常不会强制检查,运行时抛出未声明异常会调用
std::unexpected,但实际应用中很少用 - 这种写法在 C++11 后已被弃用,C++17 中已被移除
4.2 noexcept 关键字¶
noexcept 用于声明函数不会抛出异常,或根据条件决定是否抛出异常
可以用 noexcept(表达式) 检查某个表达式是否为 noexcept:
noexcept 的作用:
- 优化:编译器可对
noexcept函数做更多优化 - 异常安全:在容器移动操作等场景,只有
noexcept的移动构造/赋值才会被优先使用 - 程序终止:如果
noexcept函数抛出异常,程序会调用std::terminate()终止
5 Exceptions and Constructors¶
构造函数失败的处理方式:
- 问题:构造函数无返回值,无法像普通函数那样通过返回值表示失败
-
传统方案:
- 使用“未初始化标志”(如
bool isInitialized),需后续检查状态 - 两阶段构造(分离
Init()函数),但违反 RAII 原则。
- 使用“未初始化标志”(如
-
最佳实践:直接抛出异常,强制调用者处理错误
抛出异常的注意事项:
- 析构函数不被调用:若构造函数抛出异常,对象的析构函数不会执行(但已构造的成员子对象的析构函数会被调用)
- 资源泄漏风险:必须在抛出前手动释放已申请的资源(如内存、文件句柄)
两阶段构造(Two-stage construction):
-
在构造函数中完成基础工作
- 初始化所有成员对象
- 初始化所有基本类型成员
- 将指针初始化为
nullptr - 绝不在构造函数中申请资源
-
在
Init()函数中完成额外初始化
RAII(资源获取即初始化)
- 资源绑定对象:在构造函数中获取资源(如打开文件),在析构函数中释放
- 异常安全:若构造函数失败,通过异常中断流程,避免无效对象被使用
- 栈对象优先:强调使用栈对象(而非
new)确保内存自动管理
优先使用 RAII:
6 Exceptions and Destructors¶
析构函数调用时机:
- 正常情况:当对象离开其作用域时自动调用
- 异常情况:当发生异常进行栈展开 (stack unwinding) 时,会依次调用各作用域内对象的析构函数
关键问题:如果在析构函数执行期间又抛出新的异常,且这个析构函数本身就是由于异常处理而被调用的,程序会直接调用 std::terminate() 终止运行
重要原则:析构函数中不应该抛出异常,或者说应该确保任何可能抛出的异常在析构函数内部被捕获处理,不让异常"逃逸"出析构函数