🚨 C++ 异常机制 —— 从零到精通
💡 一句话总结:
“异常 = 程序运行时发生的‘意外情况’,C++ 用throw,try,catch三剑客来处理它,让程序不崩溃、能恢复、有尊严地报错。”
🎯 一、为什么要学异常?—— 先看“没有异常”的痛苦
👶 传统错误处理方式:返回错误码(return -1, NULL, false…)
1  | int divide(int a, int b) {  | 
❗ 问题来了:
- 如果忘记检查返回值?→ 程序逻辑错乱!
 - 如果函数要返回“正常值”和“错误码”?→ 设计混乱!
 - 如果错误发生在“深层调用”中?→ 一层层往上传,代码臃肿!
 
🌟 二、异常机制登场 —— throw, try, catch
C++ 异常机制三大关键字:
| 关键字 | 作用 | 
|---|---|
throw | 
抛出一个异常(相当于“报警”) | 
try | 
包裹可能出错的代码(“监控区”) | 
catch | 
捕获并处理异常(“接警处理”) | 
🎬 生活化比喻:餐厅点餐
- 你点了一份“牛排” → 
try { 点餐(); } - 厨房发现“牛肉卖完了” → 
throw "没牛肉了!"; - 服务员接到通知 → 
catch (string msg) { 告诉顾客 + 推荐别的菜 } - 顾客不会因为“没牛肉”掀桌子(程序不崩溃),而是优雅换菜 😊
 
🧩 三、基本语法 + 示例
✅ 1. 抛出异常:throw 表达式;
1  | void checkAge(int age) {  | 
💡
throw可以抛出 任何类型:int,string,char*, 自定义类对象等
✅ 推荐抛出 异常类对象(后文讲)
✅ 2. 捕获异常:try { ... } catch (...) { ... }
1  | int main() {  | 
🖨️ 输出:
1  | 捕获到异常:年龄不能为负数!  | 
🧱 四、异常的传播(栈展开 —— Stack Unwinding)
❓ 问题:如果异常发生在“函数调用深处”,怎么办?
1  | void func3() {  | 
🔄 栈展开过程:
func3()抛异常func3()立刻退出 → 析构局部对象func2()退出 → 析构局部对象func1()退出 → 析构局部对象main()的catch捕获异常 → 程序继续
✅ 关键点:异常会沿着调用栈“向上传播”,直到被捕获,中间函数全部退出(局部对象被析构)!
🧰 五、标准异常类(推荐使用!)
C++ 标准库提供了一套异常类(在 <stdexcept> 中),建议优先使用:
| 异常类 | 用途 | 
|---|---|
std::runtime_error | 
运行时错误(如文件打不开) | 
std::logic_error | 
逻辑错误(如传参错误) | 
std::invalid_argument | 
无效参数 | 
std::out_of_range | 
越界访问(如 vector) | 
✅ 示例:使用标准异常
1  | 
  | 
📌
.what()是std::exception的虚函数,返回错误描述字符串。
🛠️ 六、自定义异常类(高级用法)
你可以继承 std::exception 或其子类,创建自己的异常:
1  | class MyException : public std::exception {  | 
✅ 自定义异常 = 更精确的错误分类 + 更丰富的错误信息!
⚠️ 七、异常规范(C++11 起已废弃,了解即可)
老版本 C++ 支持异常规范:
1  | void func() throw(int); // 只允许抛 int 异常(已废弃)  | 
🚫 C++11 起废弃,改用
noexcept:
1  | void safeFunc() noexcept { // 承诺不抛异常  | 
🔄 八、重新抛出异常(throw;)
在 catch 块中,你可以“处理一部分,再抛出去”:
1  | void handlePartially() {  | 
✅ 用途:日志记录、资源清理、部分处理后交给上层。
🧹 九、异常安全与 RAII(重要!)
❗ 异常可能导致资源泄漏!
1  | void badExample() {  | 
✅ 解决方案:RAII + 智能指针
1  | 
  | 
🌟 RAII 原则:资源获取即初始化,绑定对象生命周期 → 异常时自动释放!
🚫 十、不要在析构函数中抛异常!
1  | class BadClass {  | 
💥 如果析构函数抛异常,且当前正在处理另一个异常 →
std::terminate()程序直接终止!
✅ 正确做法:在析构函数中用 try-catch 吞掉异常,或记录日志。
📊 十一、异常的性能开销
- 无异常时:现代编译器优化得很好,几乎无开销
 - 抛异常时:栈展开、查找 catch 块 → 开销较大(比 if-else 慢很多)
 - ✅ 建议:异常用于“真正异常”的情况(如文件打不开、网络断开),不要用于控制流程!
 
🧠 十二、异常 vs 错误码 —— 如何选择?
| 场景 | 推荐方式 | 
|---|---|
| 频繁发生的“可预期”错误(如用户输错) | ✅ 错误码 | 
| 罕见、严重、不可恢复的错误(如内存不足、文件损坏) | ✅ 异常 | 
| 库函数、API 设计 | ✅ 异常(更安全、不易忽略) | 
| 性能敏感代码(游戏循环、高频交易) | ✅ 错误码 | 
🎓 十三、完整实战示例
1  | 
  | 
📌 总结:C++ 异常机制核心要点
| 概念 | 说明 | 
|---|---|
throw | 
抛出异常,中断当前函数 | 
try-catch | 
捕获并处理异常,防止程序崩溃 | 
| 栈展开 | 异常向上传播,中间函数退出,局部对象析构 | 
| 标准异常 | 优先使用 std::exception 及其子类 | 
| 自定义异常 | 继承 std::exception,重写 what() | 
noexcept | 
声明函数不抛异常(C++11) | 
throw; | 
重新抛出当前异常 | 
| RAII | 用对象管理资源,确保异常时自动释放 | 
| 析构函数 | 绝对不要抛异常! | 
| 性能 | 异常用于“真异常”,不要滥用 | 
🧩 考考你!
下面代码会输出什么?
1  | 
  | 
✅ 答案:
1  | A 构造  | 
🌟 解释:即使
throw中断了func(),局部对象a仍会析构!这就是 栈展开 + RAII 的威力!
- 本文作者: 迪丽惹Bug
 - 本文链接: https://lyroom.github.io/2025/09/22/C-朝花夕拾-异常机制/
 - 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!