异常序一、异常的概念及使用1.1 概念1.2 异常抛出与捕获1.3 栈展开1.4 catch的匹配问题1.5 异常的重新抛出1.6 异常安全问题1.7 异常规范二、标准库序我们在C语言中主要通过错误码的形式处理错误错误码本质就是对错误信息进行分类编号拿到错误码以后还要去查询错误信息比较麻烦。因此。在C中我们引入了异常。一、异常的概念及使用1.1 概念异常处理机制允许程序中独立开发的部分能够在运行时就出现的问题进行通信并做出相应的处理异常使得我们能够将问题的检测与解决问题的过程分开程序的一部分负责检测问题的出现然后解决问题的任务传递给程序的另一部分检测环节无须知道问题的处理模块的所有细节。1.2 异常抛出与捕获程序出现问题时我们通过抛出(throw)一个对象来引发一个异常该对象的类型以及当前的调用链决定了应该由哪个catch的处理代码来处理该异常。被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。根据抛出对象的类型和内容程序的抛出异常部分告知异常处理部分到底发生了什么错误。当throw执行时throw后面的语句将不再被执行。程序的执行从throw位置跳到与之匹配的catch模块catch可能是同一函数中的一个局部的catch也可能是调用链中另一个函数中的catch控制权从throw位置转移到了catch位置。注意沿着调用链的函数可能提早退出一旦程序开始执行异常处理程序沿着调用链创建的对象都将销毁。抛出异常对象后会生成一个异常对象的拷贝因为抛出的异常对象可能是一个局部对象所以会生成一个拷贝对象这个拷贝的对象会在catch子句后销毁。1.3 栈展开throw抛出异常后try语句块中throw后面的程序将不会执行开始寻找try语句后面的catch语句找不到匹配的catch会跳出当前函数栈在外层函数栈中继续找这个过程被称为栈展开。如果在当前函数栈中找不到catch那么catch后面的语句也不会执行会直接跳出栈帧。如果找到了程序将正常运行。如果到达main函数依旧没有找到匹配的catch程序会调用标准库的terminate函数终止程序。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; }1.4 catch的匹配问题一般情况下抛出对象与catch是匹配的如果有多个可惜匹配的就选择位置更近的那个有些特殊情况允许非常量向常量的类型转换即权限缩小。允许数组转换成数组元素类型的指针函数被转换成指向函数的指针。允许从派生类向基类的转换这点非常重要。如果到main函数异常仍旧没有被匹配就会终止程序不是发生严重错误的情况下我们是不期望程序终止的所以⼀般main函数中最后都会使用catch(...)它可以捕获任意类型的异常但是不知道异常错误是什么。#include thread 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; }这是模拟实现一个服务的几个小模块每个模块都继承了Exception类因此在最后捕获异常时我们只需要捕获基类的异常即可此时无论是基类还是派生类都能被捕获派生类被捕获时会调用派生类重写的what函数来打印错误信息。1.5 异常的重新抛出catch到对象后需要对错误进行处理如果遇到需要特殊处理的错误可以将错误重新抛出到外层函数栈调用链处理。捕获异常后需要重新抛出直接throw即可。基于上面的模块这个程序模拟实现了发送消息的场景发送失败捕获异常重试三次那么就需要重新抛出。如果三次都失败了循环结束抛出对象被main函数中catch捕获打印错误信息。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; }1.6 异常安全问题异常抛出后后面的代码就不再执行前面申请了资源(内存、锁等)后面进行释放但是中间可能会抛异常就会导致资源没有释放这里由于异常就引发了资源泄漏产生安全性的问题。中间我们需要捕获异常释放资源后面再重新抛出当然后面智能指针章节讲的RAII方式解决这种问题是更好的。其次析构函数中如果抛出异常也要谨慎处理比如析构函数要释放10个资源释放到第5个时抛出异常则也需要捕获处理否则后面的5个资源就没释放也资源泄漏了。《Effctive C》第8个条款也专门讲了这个问题别让异常逃离析构函数。double Divide(int a, int b) { // 当b 0时抛出异常 if (b 0) { throw Division by zero condition!; } return(double)a / (double)b; } void Func() { // 这⾥可以看到如果发⽣除0错误抛出异常另外下⾯的array没有得到释放。 // 所以这⾥捕获异常后并不处理异常异常还是交给外层处理这⾥捕获了再 // 重新抛出去。 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; }1.7 异常规范C98中函数参数列表的后⾯接throw()表示函数不抛异常函数参数列表的后面接throw(类型1,类型2...)表示可能会抛出多种类型的异常可能会抛出的类型用逗号分割。C98的方式这种方式过于复杂实践中并不好用C11中进行了简化函数参数列表后⾯加noexcept表示不会抛出异常啥都不加表示可能会抛出异常。编译器并不会在编译时检查noexcept也就是说如果⼀个函数用noexcept修饰了但是同时又包含了throw语句或者调用的函数可能会抛出异常编译器还是会顺利编译通过的(有些编译器可能会报个警告)。但是⼀个声明了noexcept的函数抛出了异常程序会调用terminate终止程序。noexcept(expression)还可以作为⼀个运算符去检测⼀个表达式是否会抛出异常可能会则返回false不会就返回true。二、标准库C标准库也定义了一套基类为exception的异常因此我们日常写程序只需要在出函数中捕获exception即可要获取异常信息调用虚函数what函数即可。