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()
终止运行
重要原则:析构函数中不应该抛出异常,或者说应该确保任何可能抛出的异常在析构函数内部被捕获处理,不让异常"逃逸"出析构函数