C++ C++ C++ 异常处理 小米里的大麦 2025-11-30 2025-11-30 C++异常处理 1. throw / try / catch 基本语法 1. 头文件 1 2 3 4 5 #include <iostream> #include <exception> #include <stdexcept>
2. 三个关键字格式 1 2 3 4 5 6 7 8 9 10 11 12 13 try { throw std::runtime_error ("出错了" ); } catch (const std::exception& e){ } catch (...){ }
1. throw(扔错误) 格式:throw 错误内容;,例子:
1 2 3 throw "出错了" ; throw std::runtime_error ("文件打开失败" ); throw 42 ;
常用标准异常类在 <stdexcept> 头文件里:
std::logic_error:逻辑错,比如参数传错。std::runtime_error:运行时错,比如文件打不开。std::out_of_range:越界,比如数组下标超标。std::invalid_argument:无效参数。throw 发生时:
当前函数立刻停止执行(后面的代码都不跑了) 系统开始沿着调用栈 向外一层一层找 最近的 catch 每退出一层,就把这一层的所有局部对象(栈上对象)按逆序调用析构函数 找到匹配的 catch → 跳进去执行 catch 块 一直找到 main() 外面都没人 catch → 调用 std:: terminate() → 程序崩溃 所以:throw 不是立刻死,而是“带着析构一路向外跑”,没人接住才死,这机制叫“栈展开”。最大好处:不用手动删内存,局部对象自动清理。即局部变量不用担心泄露,但 new 出来的堆内存,没智能指针还是会漏。
2. try(一定范围的异常捕获) try 后面跟花括号 {},把可能出错的代码放里面。只要括号里抛出异常,后面剩下的代码就不执行了,直接跳去 catch。
1 2 3 4 5 6 try { divide (10 , 0 ); cout << "这行不会执行" << endl; }
3. catch(接错误) catch 有点像 if-else,匹配顺序是 从前往后 ,第一个匹配成功的 catch 会执行,后面的忽略。
1 2 3 4 5 6 7 8 9 10 11 12 catch (const runtime_error& e){ cout << "捕获到运行时错误:" << e.what () << endl; } catch (const exception& e){ cout << "捕获到标准错误:" << e.what () << endl; } catch (...) { cout << "捕获到未知错误" << endl; }
小技巧 :
用 const 引用 接收:catch (const exception& e),避免拷贝,效率高。 具体异常放上面,通用异常放下面。 4. 代码示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 #include <iostream> #include <stdexcept> using namespace std;void divide (int a, int b) { if (b == 0 ) { throw runtime_error ("除数不能为 0" ); } cout << "结果:" << a / b << endl; } int main () { try { divide (10 , 0 ); cout << "这行不会执行" << endl; } catch (const runtime_error& e) { cout << "捕获到运行时错误:" << e.what () << endl; } catch (const exception& e) { cout << "捕获到标准错误:" << e.what () << endl; } catch (...) { cout << "捕获到未知错误" << endl; } cout << "程序继续运行" << endl; return 0 ; }
3. 注意事项小结 catch 用引用 :能写 catch (const exception& e),就别写 catch (exception e)。避免拷贝,效率高。顺序很重要 :具体的异常放上面,通用的放下面。比如 runtime_error 要在 exception 前面。throw 啥都行 :可以扔 int,扔字符串,最好扔异常对象。catch (…) ) :这是兜底的,放最后。啥错误都能接,但不知道具体是啥错。头文件别漏 :不用 <stdexcept> 编译器可能不认识 runtime_error。标准异常类 :std::exception 是基类。常用的有 runtime_error(运行时错)、logic_error(逻辑错)、out_of_range(越界)。2. 异常传播规则(栈展开、析构函数调用) 1. 什么是异常传播 当 throw 发生时,程序会 沿着调用链往上找 catch ,这个过程叫 栈展开(Stack Unwinding) 。简单说就是:从当前函数一层一层往回跑,看看谁能接住这个异常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 void f3 () { throw std::runtime_error ("f3 失败" ); } void f2 () { f3 (); } void f1 () { f2 (); } int main () { try { f1 (); } catch (const std::exception& e) { std::cout << e.what () << '\n' ; } }
执行顺序:
f3() 抛出异常f2() 没 catch,继续往上f1() 没 catch,继续往上main() 有 catch,捕获成功2. 栈展开时发生了什么? 每跳出一层函数,那一层的 局部对象会被自动销毁 (调用析构函数)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 struct Widget { ~Widget () { cout << "Widget 析构\n" ; } }; void level3 () { Widget w; throw std::runtime_error ("错!" ); } void level2 () { Widget w; level3 (); } int main () { try { level2 (); } catch (...) { cout << "捕获\n" ; } }
输出:
1 2 3 Widget 析构 // level3 的 w Widget 析构 // level2 的 w 捕获
发生异常后,程序会从抛出点一路“回退”调用栈。
当前作用域里已经构造成功的局部对象,会按逆序调用析构函数。 这就是 RAII 能防泄漏的根本原因。 还没构造成功的对象,不会析构。 这就是 RAII 的基础 :利用析构函数自动释放资源。
3. 析构函数与异常 重要规则:析构函数绝对不能抛异常!如果析构函数在栈展开期间再抛异常,程序会直接 std::terminate()。
为什么?
栈展开时已经在处理一个异常了 如果析构函数又抛异常,程序不知道该处理哪个 结果:直接调用 std::terminate() 崩溃 1 2 3 4 5 6 7 8 class Bad { public : ~Bad () noexcept { } };
正确做法 :析构函数里如果可能出错,捕获并吞掉:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Good { public : ~Good () noexcept { try { } catch (...) { } } };
4. 构造函数中的异常 构造函数抛异常时,已构造的部分会被自动销毁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Widget { public : Widget (int size) { if (size < 0 ) throw std::invalid_argument ("size 不能为负" ); data = new int [size]; } ~Widget () { delete [] data; } private : int * data; };
实用建议 :用 RAII 管理资源,别直接 new/delete。
3. noexcept 1. 基本语法 noexcept 表示 “这个函数 承诺不抛异常 ”。
1 2 3 4 5 6 7 void func () noexcept ; void func () noexcept (true ) ; void func () noexcept (false ) ; template <typename T>void swap (T& a, T& b) noexcept (std::is_nothrow_swappable_v<T>) ;
2. 违反 noexcept 会怎样 noexcept 函数里如果真的抛异常,程序不会继续找 catch,而是直接 std::terminate()。
1 2 3 4 void bad () noexcept { throw std::runtime_error ("Oops!" ); }
如果调用 bad() 并抛出异常,程序会直接调用 std::terminate(),二话不说直接崩溃 。
教训:只有 100% 确定不会抛异常,才加 noexcept。
3. noexcept 有什么用的? 1. 性能优化 编译器知道函数不抛异常后:
1 2 3 4 5 std::vector<std::string> v; std::vector<MyClass> w;
2. STL 容器依赖 noexcept 做决策 std::vector 扩容时:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void reallocate () { if (std::is_nothrow_move_constructible<T>::value) { newData = static_cast <T*>(::operator new (n)); new (newData + i) T (std::move (oldData[i])); } else { newData[i] = oldData[i]; } }
3. 接口契约 告诉调用者:“这个函数安全,不会因异常中断”。对于析构函数、移动构造/赋值、swap,强烈建议加 noexcept 。
4. 常见使用场景 1. 移动构造函数 / 移动赋值 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class MyClass { public : MyClass (MyClass&& other) noexcept : ptr (other.ptr) { other.ptr = nullptr ; } MyClass& operator =(MyClass&& other) noexcept { if (this != &other) { delete ptr; ptr = other.ptr; other.ptr = nullptr ; } return *this ; } private : int * ptr; };
2. swap 函数 1 2 3 4 5 6 7 8 9 10 11 class MyStack { public : void swap (MyStack& other) noexcept { data.swap (other.data); } private : std::deque<int > data; };
3. 析构函数 1 2 3 4 5 6 7 8 class Resource { public : ~Resource () noexcept = default ; };
5. 为什么移动构造要尽量 noexcept 标准容器(尤其 std::vector)扩容时会优先选择“更安全”的路径。
元素移动构造是 noexcept:容器更敢用移动,性能通常更好。 不是 noexcept:容器可能退回拷贝策略来保异常安全。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Buffer { public : Buffer (Buffer&& other) noexcept { data_ = other.data_; other.data_ = nullptr ; } ~Buffer () noexcept { delete [] data_; } private : int * data_ = nullptr ; };
6. 实用建议 析构函数:默认就应当是 noexcept。 移动构造、移动赋值、swap:能保证不抛就加 noexcept。 只有 100% 确定不抛,才写 noexcept。 4. std:: exception 继承体系(常用标准异常) 1. 常见继承链 1 2 3 4 5 6 7 8 9 10 std::exception ├── std::logic_error │ ├── std::invalid_argument │ ├── std::domain_error │ ├── std::length_error │ └── std::out_of_range └── std::runtime_error ├── std::range_error ├── std::overflow_error └── std::underflow_error
2. 常用异常类 异常类 什么时候用 std::invalid_argument参数值不合法 std::out_of_range下标越界访问 std::runtime_error运行时出错,文件打不开、网络断连等 std::overflow_error整数溢出、加法溢出 std::length_error比如 string 太长,超过 max_size() std::logic_error代码逻辑前提被破坏
3. 代码示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 #include <stdexcept> #include <vector> #include <iostream> using namespace std;void process (vector<int >& v, int index) { if (index < 0 ) throw invalid_argument ("索引不能为负" ); if (index >= v.size ()) throw out_of_range ("索引超出范围" ); v[index] *= 2 ; } int main () { vector<int > v = {1 , 2 , 3 }; try { process (v, 100 ); } catch (const out_of_range& e) { cout << "越界:" << e.what () << endl; } catch (const invalid_argument& e) { cout << "参数错:" << e.what () << endl; } return 0 ; }
5. 自定义异常类 1. 最简单写法 继承 std::runtime_error 或 std::logic_error:
1 2 3 4 5 6 7 8 9 #include <stdexcept> #include <string> class MyError : public std::runtime_error{ public : explicit MyError (const std::string& msg) : std::runtime_error(msg) { }};
使用:
1 throw MyError ("我的错误信息" );
2. 更完整的写法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <stdexcept> #include <string> class NetworkError : public std::runtime_error{ public : NetworkError (const std::string& msg, int code) : std::runtime_error (msg), errorCode (code) {} int code () const { return errorCode; } private : int errorCode; }; class ConfigError : public std::logic_error{ public : explicit ConfigError (const std::string& msg) : std::logic_error(msg) { }};
3. 直接继承 std:: exception 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include <exception> #include <string> class MyException : public std::exception{ public : explicit MyException (const std::string& msg) noexcept : message(msg) { } const char * what () const noexcept override { return message.c_str (); } private : std::string message; };
4. 实用建议 继承 std::runtime_error 或 std::logic_error(推荐) 提供有意义的错误信息 如果需要额外数据,加成员变量和 getter what() 要覆盖,返回错误描述析构函数要 noexcept 6. catch(…) 的危险性与替代方案 1. catch(…) 是什么? catch(...) 能捕获 所有 异常:
1 2 3 4 5 6 7 8 try { } catch (...){ }
2. 危险在哪? 不知道具体是什么异常 ,没法正确处理:
常见错误:
1 2 3 4 5 6 7 8 9 10 11 try { doSomething (); } catch (...){ } saveToDatabase ();
3. 正确用法 1. 重新抛出 1 2 3 4 5 6 7 8 9 10 try { riskyOperation (); } catch (...){ logError (); throw ; }
2. 转换为已知类型 1 2 3 4 5 6 7 8 9 10 11 12 13 14 try { process (); } catch (const std::exception& e){ cerr << "错误:" << e.what () << endl; } catch (...){ throw std::runtime_error ("未知错误" ); }
3. 资源清理(配合 RAII) 1 2 3 4 5 6 7 8 9 10 try { } catch (...){ throw ; }
4. 替代方案 现代 C++ 推荐:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 std::optional<int > divide (int a, int b) { if (b == 0 ) return std::nullopt ; return a / b; } std::expected<int , std::string> divide (int a, int b) { if (b == 0 ) return std::unexpected ("除数不能为0" ); return a / b; } std::error_code divide (int a, int b, int & result) ;
7. RAII 与异常安全保证 1. 什么是 RAII? RAII = Resource Acquisition Is Initialization,翻译:资源获取即初始化。
构造时拿资源。 析构时放资源。 异常发生时,栈展开会自动调用析构。 核心思想:用对象的构造函数获取资源,析构函数释放资源(对象生命周期绑定资源生命周期) 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class FileHandle { public : FileHandle (const string& filename) { file = fopen (filename.c_str (), "r" ); if (!file) throw std::runtime_error ("打不开" ); } ~FileHandle () { if (file) fclose (file); } private : FILE* file; };
使用:
1 2 3 4 5 6 7 void readFile () { FileHandle fh ("data.txt" ) ; }
2. 异常安全三级保证 Basic:异常后资源不泄漏,对象仍然有效,但值可能变化。Strong:要么成功,要么像没执行过(无副作用)。Nothrow:承诺绝不抛异常。1. 基本保证(Basic Guarantee) 资源不泄漏,对象处于有效状态(即使不清楚是什么状态)。
1 2 3 4 5 6 7 void doSomething () { auto p = std::make_unique <Widget>(); }
3. 强保证(Strong Guarantee) 操作要么 完全成功 ,要么 完全没发生 。
1 2 3 4 5 6 7 8 9 10 11 12 void addCustomer (Customer& c) { auto transaction = db.beginTransaction (); db.insert (c); log.write (c); transaction.commit (); }
3. Nothrow 保证 绝不抛异常 。用于析构函数、swap、移动操作。
1 2 3 4 5 6 7 8 9 class Widget { public : ~Widget () noexcept {} void swap (Widget& other) noexcept {} Widget (Widget&&) noexcept {} Widget& operator =(Widget&&) noexcept {} };
3. 如何实现强保证? 用 复制-交换(Copy-and-Swap) 模式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Widget { public : void update (const Data& newData) { Widget temp (*this ) ; temp.apply (newData); swap (temp); } void swap (Widget& other) noexcept { data.swap (other.data); } private : Data data; };
8. 异常下的资源管理 1. 智能指针(必用) 智能指针是现代 C++ 的 标配 :
1 2 3 4 5 6 7 auto p1 = std::make_unique <Widget>(); auto p2 = std::make_shared <Widget>(); std::unique_ptr<Widget> p1 (new Widget) ;std::shared_ptr<Widget> p2 (new Widget) ;
为什么用智能指针?
异常发生时,自动释放内存 不用手动 delete 不会泄漏 2. Scope Guard(作用域守卫) std::scope_exit(C++17)是 RAII 版 try-finally :
1 2 3 4 5 6 7 8 9 10 11 #include <scope> void process () { auto guard = std::scope_exit ([&] { cout << "无论成功还是失败,这行都会执行\n" ; }); }
类似的还有:
std::scope_success(仅成功时执行)std::scope_failure(仅失败时执行)3. 传统写法(了解即可) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void oldStyle () { FILE* f = fopen ("data.txt" , "w" ); if (!f) throw runtime_error ("打不开" ); try { } catch (...) { fclose (f); throw ; } fclose (f); }
现在不推荐 :用智能指针或 scope guard 更简洁、更安全。
4. 智能指针补充(点到为止) 关于智能指针的详细内容,会在其他文章中讲解。这里记住:能用 make_unique / make_shared 就用,别直接 new 。
9. terminate() / set_terminate / unexpected() 1. std:: terminate() std::terminate() 是 异常处理系统的紧急出口 ,即“程序无法继续安全运行”时的最后处理。
调用时机:
析构函数抛异常 noexcept 函数内部抛异常异常传播中找不到匹配的 catch 析构函数在栈展开过程中再抛异常 1 2 3 4 5 6 7 8 9 10 void bad () noexcept { throw std::runtime_error ("boom" ); } int main () { bad (); cout << "不会到这里" << endl; }
2. std:: set_terminate() 可以自定义 terminate 的行为,用于打日志、上报、快速失败。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <exception> #include <iostream> using namespace std;void myTerminate () { cout << "程序要崩溃了,做最后的清理...\n" ; exit (1 ); } int main () { set_terminate (myTerminate); throw 1 ; }
用途 :
3. std:: unexpected()(了解即可) unexpected() 会在 动态异常规范 违规时调用(C++98 遗留特性,现代代码基本不用),这套机制在 C++11 开始被弃用,在 C++17 已移除 。
1 2 3 4 5 void oldStyle () throw (std::runtime_error) ; void modernStyle () noexcept ;
现代 C++ 不推荐动态异常规范,noexcept 已经完全够用。
10. 现代 C++ 异常处理小结 1. 推荐做法 析构函数、move 构造/赋值、swap → 必须 noexcept优先用 make_unique / make_shared → 别直接 new简单情况用 std:: optional → 避免异常复杂情况用 std:: expected (C++23)→ 错误码风格库边界用异常,内部模块倾向 error code 永远不要在 catch 里吞掉异常不处理 2. 不推荐做法 catch (...) 盲目吞掉所有异常在析构函数里抛异常 不用智能指针,手动 new/delete 用异常做流程控制(if-else 更合适) 异常是工具,不是洪水猛兽。用好 RAII,遵循规范,程序稳如泰山。