🚨 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 许可协议。转载请注明出处!