十四、异常

📅 2026/7/12 22:03:09
十四、异常
注这个是博主复习使用的专题仅适用于自己以及学习过C知识点的同学文章目录前言一、C语言传统的处理错误的方式二、C异常格式三、异常的用法3.1. 异常的抛出和捕获3.2. 栈展开3.3. 查找匹配的处理代码3.4. 异常重新抛出3.5. 异常安全问题3.6. 异常规范四、标准库异常体系五、异常的优缺点前言注这个是博主复习使用的专题仅适用于自己以及学习过C知识点的同学。一、C语言传统的处理错误的方式传统的错误处理机制终止程序如assert。缺陷用户难以接受。如发生内存错误除0错误时就会终止程序。返回错误码。缺陷需要程序员自己去查找对应的错误。如系统的很多库的接口函数都是通过把错误码放到errno中表示错误。C标准库中setjmp和longjmp组合。不常用实际中C语言基本都是使用返回错误码的方式处理错误部分情况下使用终止程序处理非常严重的错误。二、C异常格式异常处理机制允许程序中独立开发的部分能够在运行时就出现的问题进行通信并做出相应的处理异常使得我们能够将问题的检测与解决问题的过程分开程序的一部分负责检测问题的出现然后解决问题的任务传递给程序的另⼀部分检测环节无须知道问题的处理模块的所有细节。throw当程序出现问题时可以通过throw关键字抛出一个异常。trytry块中放置的是可能抛出异常的代码该代码块在执行时将进行异常错误检测try块后面通常跟着一个或多个catch块。catch如果try块中发生错误则可以在catch块中定义对应要执行的代码块。使用try/catch语句的语法如下所示try { //被保护的代码 } catch (ExceptionName e1) { //catch块 } catch (ExceptionName e2) { //catch块 } catch (ExceptionName eN) { //catch块 }三、异常的用法3.1. 异常的抛出和捕获程序出现问题时我们通过抛出 (throw) 一个对象来引发一个异常该对象的类型以及当前的调用链决定了应该有哪个 catch 的处理代码来处理该异常。抛出异常对象后会生成一个异常对象的拷贝因为抛出的异常对象可能是一个局部对象所以会生成一个拷贝对象这个拷贝对象会在 catch 子句后销毁。(这里的处理类似于函数的传值返回)该选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。根据抛出对象的类型和内容程序的抛出异常部分告知异常处理部分到底发生了什么错误。当 throw 执行时throw 后面的语句将不会执行。程序的执行从 throw 位置跳到 与之匹配的 catch 模块catch可能是同一函数中的一个局部的catch也可能是调用链中另一个函数的catch控制权从throw位置转移到了catch位置。这里还有两个重要的含义1、沿着调用链的函数可能提早退出。2、一旦程序开始执行异常处理程序沿着调用链创建的对象都将销毁。(栈展开过程中所有局部对象会被自动销毁保证资源正确释放。)catch(...)可以捕获任意类型的异常但捕获后无法知道异常错误是什么。实际异常的抛出和捕获的匹配原则有个例外捕获和抛出的异常类型并不一定要完全匹配可以抛出派生类对象使用基类进行捕获这个在实际中非常有用。3.2. 栈展开在函数调用链中异常栈展开的匹配原则抛出异常后程序暂停当前函数的执行开始寻找与之匹配的 catch 子句首先检查 throw 本身是否在 try 内部如果在查找匹配的 catch 语句如果有匹配的则跳到 catch 的地方进行处理。如果当前函数中没有try/catch子句或者有 try / catch 子句但是类型不匹配则退出当前函数继续在外层调用函数链中查找上述查找的 catch 过程被称作栈展开。如果到达 main 函数依旧没有找到匹配的 catch 子句程序会调用标准库的 terminate 函数终止程序。如果找到匹配的 catch 子句处理后catch 子句代码会继续执行。double Divide(int a, int b) { try { // b 0 时抛出异常 if (b 0) { string s(Divide by zero condition!); throw s; } else { return ((double)a / (double)b); } } catch(int errid) { cout errid endl; } return 0; } void Func() { int len, time; cin len time; try { cout Divide(len, time) endl; } catch(const char* errmsg) { cout errmsg endl; } cout __FUNCTION__ : __LINE__ 行执行 endl; } int main() { while (1) { try { Func(); } catch(const string errmsg) { cout errmsg endl; } } return 0; }有三个函数有 Func()Divide () 和 main() 函数在 main() 中 调用 Func() 函数Func() 函数 调用 Divide() 当输入 0 的时候会在 Divide() 函数中抛出一个异常并在 main() 中调用 catch 语句捕获。栈展开过程如下首先检查 throw 本身是否在 try 块内部如果是再查找匹配的 catch 语句如果有匹配的则处理。没有则退出当前函数栈继续在调用函数的栈中进行查找不断重复上述过程若到达 main 函数 的栈依旧没有匹配的则终止程序。3.3. 查找匹配的处理代码#includethread class Exception { public : Exception(const string errmsg, int id) : _errmsg(errmsg) , _id(id) {} virtual string what() const { return _errmsg; } int getid() const { return _id; } protected: string _errmsg; int _id; }; class SqlException : public Exception { public : SqlException(const string errmsg, int id, const string sql) : Exception(errmsg, id) , _sql(sql) {} virtual string what() const { string str SqlException:; str _errmsg; str -; str _sql; return str; } private: const string _sql; }; class CacheException : public Exception { public : CacheException(const string errmsg, int id) : Exception(errmsg, id) {} virtual string what() const { string str CacheException:; str _errmsg; return str; } }; class HttpException : public Exception { public : HttpException(const string errmsg, int id, const string type) : Exception(errmsg, id) , _type(type) {} virtual string what() const { string str HttpException:; str _type; str :; str _errmsg; return str; } private: const string _type; }; void SQLMgr() { if (rand() % 7 0) { throw SqlException(权限不足, 100, select * from name 张三); } else { cout SQLMgr 调用成功 endl; } } void CacheMgr() { if (rand() % 5 0) { throw CacheException(权限不足, 100); } else if (rand() % 6 0) { throw CacheException(数据不存在, 101); } else { cout CacheMgr 调用成功 endl; } SQLMgr(); } void HttpServer() { if (rand() % 3 0) { throw HttpException(请求资源不存在, 100, get); } else if (rand() % 4 0) { throw HttpException(权限不足, 101, post); } else { cout HttpServer调用成功 endl; } CacheMgr(); } int main() { srand(time(0)); while (1) { this_thread::sleep_for(chrono::seconds(1)); try { HttpServer(); } catch(const Exception e) // 这里捕获基类基类对象和派生类对象都可以被捕获 { cout e.what() endl; } catch(...) { cout Unkown Exception endl; } } return 0; }3.4. 异常重新抛出在 C 异常处理中catch 块捕获到异常后有时我们并不想完全处理这个异常而只是做一些局部的工作比如记录日志、释放某些资源然后希望将同一个异常继续向上层抛出让调用链中更外层的catch来处理。这种操作称为重新抛出rethrow通过 throw;语句实现。下面程序模拟展示了聊天时发送消息发送失败补货异常但是可能在电梯地下室等场景手机信号不好则需要多次尝试如果多次尝试都发送不出去则就需要捕获异常再重新抛出其次如果不是网络差导致的错误捕获后也要重新抛出。#includethread class Exception { public : Exception(const string errmsg, int id) : _errmsg(errmsg) , _id(id) {} virtual string what() const { return _errmsg; } int getid() const { return _id; } protected: string _errmsg; int _id; }; class SqlException : public Exception { public : SqlException(const string errmsg, int id, const string sql) : Exception(errmsg, id) , _sql(sql) {} virtual string what() const { string str SqlException:; str _errmsg; str -; str _sql; return str; } private: const string _sql; }; class CacheException : public Exception { public : CacheException(const string errmsg, int id) : Exception(errmsg, id) {} virtual string what() const { string str CacheException:; str _errmsg; return str; } }; class HttpException : public Exception { public : HttpException(const string errmsg, int id, const string type) : Exception(errmsg, id) , _type(type) {} virtual string what() const { string str HttpException:; str _type; str :; str _errmsg; return str; } private: const string _type; }; void SQLMgr() { if (rand() % 7 0) { throw SqlException(权限不足, 100, select * from name 张三); } else { cout SQLMgr 调用成功 endl; } } void CacheMgr() { if (rand() % 5 0) { throw CacheException(权限不足, 100); } else if (rand() % 6 0) { throw CacheException(数据不存在, 101); } else { cout CacheMgr 调用成功 endl; } SQLMgr(); } void HttpServer() { if (rand() % 3 0) { throw HttpException(请求资源不存在, 100, get); } else if (rand() % 4 0) { throw HttpException(权限不足, 101, post); } else { cout HttpServer调用成功 endl; } CacheMgr(); } void _SeedMsg(const string s) { if (rand() % 2 0) { throw HttpException(网络不稳定发送失败, 102, put); } else if (rand() % 7 0) { throw HttpException(你已经不是对象的好友发送失败, 103, put); } else { cout 发送成功 endl; } } void SendMsg(const string s) { // 发送消息失败则再重试3次 for (size_t i 0; i 4; i) { try { _SeedMsg(s); break; } catch(const Exception e) { // 捕获异常if中是102号错误网络不稳定则重新发送 // 捕获异常else中不是102号错误则将异常重新抛出 if (e.getid() 102) { // 重试三次以后否失败了则说明网络太差了重新抛出异常 if (i 3) throw; cout 开始第 i 1 重试 endl; } else { throw; } } } } int main() { srand(time(0)); string str; while (cin str) { try { SendMsg(str); } catch(const Exception e) { cout e.what() endl endl; } catch(...) { cout Unkown Exception endl; } } return 0; }3.5. 异常安全问题异常抛出后后面的代码就不再执行前面申请了资源(内存、锁等)后面进行释放但是中间可能会抛异常就会导致资源没有释放这里由于异常就引发了资源泄漏产生安全性的问题。中间我们需要捕获异常释放资源后面再重新抛出当然后面智能指针章节讲的RAII方式解决这种问题是更好的。其次析构函数中如果抛出异常也要谨慎处理比如如析构函数要释放10个资源释放到第5个时抛出异常则也需要捕获处理否则后面的5个资源就没释放也资源泄漏了。例如下面这里可以看到如果发生除0错误抛出异常另外下面的array没有得到释放。所以这里捕获异常后并不处理异常异常还是交给外层处理这里捕获了再重新抛出去。double Divide(int a, int b) { // 当b 0时抛出异常 if (b 0) { throw Division by zero condition!; } return(double)a / (double)b; } void Func() { int* array new int[10]; try { int len, time; cin len time; cout Divide(len, time) endl; } catch(...) { // 捕获异常释放内存 cout delete [] array endl; delete[] array; throw; // 异常重新抛出捕获到什么抛出什么 } cout delete [] array endl; delete[] array; } int main() { try { Func(); } catch(const char* errmsg) { cout errmsg endl; } catch(const exception e) { cout e.what() endl; } catch(...) { cout Unkown Exception endl; } return 0; }3.6. 异常规范对于用户和编译器而言预先知道某个程序会不会抛出异常大有裨益知道某个函数是否会抛出异常有助于简化调用函数的代码。C98中函数参数列表的后面接throw()表示函数不抛异常函数参数列表的后面接throw(类型1,类型2...)表示可能会抛出多种类型的异常可能会抛出的类型用逗号分割。C98的方式这种方式过于复杂实践中并不好用C11中进行了简化函数参数列表后面加noexcept表示不会抛出异常啥都不加表示可能会抛出异常。编译器并不会在编译时检查noexcept也就是说如果⼀个函数用noexcept修饰了但是同时又包含了throw语句或者调用的函数可能会抛出异常编译器还是会顺利编译通过的(有些编译器可能会报个警告)。但是⼀个声明了noexcept的函数抛出了异常程序会调⽤ terminate 终止程序。noexcept(expression)还可以作为⼀个运算符去检测⼀个表达式是否会抛出异常可能会则返回false不会就返回true// C98 // 这里表示这个函数只会抛出bad_alloc的异常 void* operator new (std::size_t size) throw (std::bad_alloc); // 这里表示这个函数不会抛出异常 void* operator delete (std::size_t size, void* ptr) throw(); // C11 size_type size() const noexcept; iterator begin() noexcept; const_iterator begin() const noexcept; double Divide(int a, int b) noexcept { // 当b 0时抛出异常 if (b 0) { throw Division by zero condition!; } return(double)a / (double)b; } int main() { try { int len, time; cin len time; cout Divide(len, time) endl; } catch(const char* errmsg) { cout errmsg endl; } catch(...) { cout Unkown Exception endl; } int i 0; cout noexcept(Divide(1, 2)) endl; cout noexcept(Divide(1, 0)) endl; cout noexcept(i) endl; return 0; }四、标准库异常体系C标准库当中的异常也是一个基础体系其中exception就是各个异常类的基类我们可以在程序中使用这些标准的异常它们之间的继承关系如下下表是对上面继承体系中出现的每个异常的说明异常描述std::exception该异常是所有标准C异常的父类std::bad_alloc该异常可以通过 new 抛出std::bad_cast该异常可以通过 dynamic_cast 抛出std::bad_exception这在处理C程序中无法预期的异常时非常有用std::bad_typeid该异常可以通过 typeid 抛出std::logic_error理论上可以通过读取代码来检测到异常std::domain_error当使用了一个无效的数字域时会抛出该异常。std::invalid_argument当使用了无效的参数时会抛出该异常std::length_error当创建了太长的 std::string 时会抛出该异常std::out_of_range该异常可以通过方法抛出例如 std::vector 和 std::bitset::operatorstd::runtime_error理论上不可以通过读取代码来检测到的异常std::overflow_error当发生数字上溢时会抛出该异常std::range_error当尝试存储超出范围的值时会抛出该异常std::underflow_error当发生数学下溢时会抛出该异常说明一下exception类的what成员函数和析构函数都定义成了虚函数方便子类对其进行重写从而达到多态的效果。实际中我们也可以去继承exception类来实现自己的异常类但实际中很多公司都会自己定义一套异常继承体系。五、异常的优缺点异常的优点异常对象定义好了相比错误码的方式可以清晰准确的展示出错误的各种信息甚至可以包含堆栈调用等信息这样可以帮助更好的定位程序的bug。返回错误码的传统方式有个很大的问题就是在函数调用链中深层的函数返回了错误那么我们得层层返回错误码最终最外层才能拿到错误。很多的第三方库都会使用异常比如boost、gtest、gmock等等常用的库如果我们不用异常就不能很好的发挥这些库的作用。很多测试框架也都使用异常因此使用异常能更好的使用单元测试等进行白盒的测试。部分函数使用异常更好处理比如T operator这样的函数如果pos越界了只能使用异常或者终止程序处理没办法通过返回值表示错误。异常的缺点异常会导致程序的执行流乱跳并且非常的混乱这会导致我们跟踪调试以及分析程序时比较困难。异常会有一些性能的开销当然在现代硬件速度很快的情况下这个影响基本忽略不计。C没有垃圾回收机制资源需要自己管理。有了异常非常容易导致内存泄露、死锁等异常安全问题。这个需要使用RAII来处理资源的管理问题学习成本比较高。C标准库的异常体系定义得不够好导致大家各自定义自己的异常体系非常的混乱。异常尽量规范使用否则后果不堪设想随意抛异常也会让外层捕获的用户苦不堪言。异常接口声明不是强制的对于没有声明异常类型的函数无法预知该函数是否会抛出异常。但总体而言异常的利大于弊所以工程中我们还是鼓励使用异常的并且OO的语言基本都使用异常处理错误这也可以看出这是大势所趋。完