【C++】异常

【C++】异常 异常1、C异常概念异常是一种处理错误的方式当一个函数发现自己无法处理的错误时就可以抛出异常让函数的直接或间接的调用者处理这个错误异常使问题更简单程序的一部分负责检测问题的出现然后解决问题的任务传递给程序的另一部分C语言主要通过错误码的形式处理错误错误码本质就是对错误信息进行分类编号拿到错误码以后还要去查询错误信息比较麻烦。异常时抛出⼀个对象这个对象可以函数更全面的各种信息2、异常的抛出与捕获throw当程序出现问题时我们通过抛出throw一个对象引发一个异常catch在想处理问题的地方通过异常处理程序捕获异常catch是专门用来接住、处理throw抛出来的异常错误的代码块trytry块中的代码标识将被激活的特定异常它后面通常跟一个或者多个catch块3、异常的使用3.1 栈展开抛出异常后程序暂停当前函数的执行开始寻找与之匹配的catch子句首先检查throw本身是否在try块内部如果在就查找匹配的catch语句如果有匹配的则跳到catch的地方进行处理如果当前没有匹配的try/catch子句则退出当前的函数栈或者有try/catch子句但类型不匹配则退出当前函数继续在外层调用函数链中查找上述查找的catch过程被称为栈展开如果达到main函数的栈依旧没有匹配的catch子句程序会终止如果找到匹配的catch子句处理后catch子句代码会继续执行如图的三个函数func1()、func2()、func3()。在func2()中调用func1()func3()中调用func2()main()中调用func3()并在func1()中抛出一个异常在main()中用catch语句捕获doubleDivide(inta,intb){// 当b 0时抛出异常if(b0){strings(Divide by zero condition!);throws;}else{return((double)a/(double)b);}cout__FUNCTION__:__LINE__行执行endl;return0;}voidFunc(){intlen,time;cinlentime;coutDivide(len,time)endl;}intmain(){try{Func();}catch(conststringerrmsg){couterrmsgendl;}catch(interrid){couterridendl;}return0;}输入1 0输出结果Divide by zero condition!栈展开的过程b0当前函数终止throw之后的代码不会执行Divide函数内没有try-catch捕获触发栈展开回到Func函数Func中调用Divide的语句被异常打断后面的cout语句不会执行Func函数中无try-catch继续栈展开销毁Func栈帧异常继续向上抛回到main函数的try{}代码块内main函数try块捕捉异常匹配catch一般情况下抛出对象和catch是类型完全匹配的如果有多个类型匹配的就选择离他位置更近的那个在Divide函数中加入try-catchdoubleDivide(inta,intb){try{// 当b 0时抛出异常if(b0)// 输入1 0{strings(Divide by zero condition!);throws;// 抛出异常}else{return((double)a/(double)b);}//... fxx()}catch(constints){coutsendl;}catch(conststringerrmsg)// 抛出string类型匹配本catch块执行{couterrmsgendl;}// 本地catch捕获异常栈展开终止继续向下执行函数剩余代码cout__FUNCTION__:__LINE__行执行endl;return0;// 返回0给上层Func本行执行}voidFunc(){intlen,time;cinlentime;coutDivide(len,time)endl;// 调用Divide拿到返回值0输出0执行cout__FUNCTION__:__LINE__行执行endl;// 无异常抛出本行正常执行}intmain(){try{Func();}// 异常被Divide本地捕获不会进入main的catch不执行catch(conststringerrmsg){couterrmsgendl;}catch(interrid){couterridendl;}return0;}输入1 0输出结果Divide by zero condition!Divide:30行执行0Func:42行执行在Divide函数中已有try-catch模块并且离异常最近不需要main函数中的catch块注意throw抛出异常后try内的其余代码不会执行了如果本函数内有匹配的catch接住异常那么try-catch结构之外、函数剩下的代码会正常执行如果本函数内没有匹配的catch接住异常那么剩下的所有代码都不会执行egtry{throws;// 直接跳出trytry内剩下代码全不跑// 下面这些永远不执行else分支return;//...fxx();}catch(stringerr){couterr;// 捕获异常执行完这个块}// try-catch外面的代码不受异常影响正常运行cout__FUNCTION__执行endl;return0;try{string s;throws;// 抛出string}catch(constint)// 只能捕获int匹配失败{}// 下面两行【完全不会执行】直接退出函数向上抛异常cout__FUNCTION__执行endl;return0;3.2 查找匹配的处理代码#includethread/*一般大型项目程序才会使用异常下面我们模拟设计一个服务的几个模块 每个模块的继承都是Exception的派生类每个模块可以添加自己的数据 最后捕获时我们捕获基类就可以*/// 父类 Exception所有异常的通用模板classException{public:// 构造函数接收错误描述、错误编号Exception(conststringerrmsg,intid):_errmsg(errmsg),_id(id){}// virtual虚函数打印异常信息子类会重写这个函数virtualstringwhat()const{return_errmsg;}// 获取错误编号intgetid()const{return_id;}protected:string _errmsg;int_id;};// 构造除了父类要的errmsg、id额外接收出错SQL语句classSqlException:publicException{public:SqlException(conststringerrmsg,intid,conststringsql):Exception(errmsg,id),_sql(sql){}// 重写what()拼接专属报错格式virtualstringwhat()const{string strSqlException:;str_errmsg;str-;str_sql;returnstr;}private:conststring _sql;//};classCacheException:publicException{public:CacheException(conststringerrmsg,intid):Exception(errmsg,id){}virtualstringwhat()const{string strCacheException:;str_errmsg;returnstr;}};classHttpException:publicException{public:HttpException(conststringerrmsg,intid,conststringtype):Exception(errmsg,id),_type(type){}virtualstringwhat()const{string strHttpException:;str_type;str:;str_errmsg;returnstr;}private:conststring _type;};voidSQLMgr(){if(rand()%70){throwSqlException(权限不足,100,select * from name 张三);}else{coutSQLMgr 调用成功endl;}}voidCacheMgr(){if(rand()%50){throwCacheException(权限不足,100);}elseif(rand()%60){throwCacheException(数据不存在,101);}else{coutCacheMgr 调用成功endl;}SQLMgr();}voidHttpServer(){if(rand()%30){throwHttpException(请求资源不存在,100,get);}elseif(rand()%40){throwHttpException(权限不足,101,post);}else{coutHttpServer调用成功endl;}CacheMgr();}// 健壮性intmain(){srand(time(0));while(1)// 无限循环持续模拟服务运行{this_thread::sleep_for(chrono::seconds(1));try{HttpServer();// 所有业务逻辑入口}catch(constExceptione)// 这里捕获基类基类对象和派生类对象都可以被捕获{coute.what()endl;// 多态}catch(...)// 兜底捕获任意其他异常{cout未知异常endl;}// ...}return0;}上面的代码中父类Exception是通用异常模板所有模块报错都继承它三个业务派生异常SqlException数据库 SQL 报错、CacheException缓存模块报错、HttpException网络HTTP请求报错额外存请求方式允许从派生类向基类类型的转换抛出子类的对象catch用父类的引用/指针可以直接匹配4、异常重新抛出有可能单个的catch不能完全处理一个异常其中的某种异常错误需要进行特殊的处理其他错误则重新抛出异常给外层调用链处理catch则可以通过重新抛出将异常传递给更上层的函数进行处理// 下面程序模拟展示了聊天时发送消息发送失败补货异常但是可能在// 电梯地下室等场景手机信号不好则需要多次尝试如果多次尝试都发// 送不出去则就需要捕获异常再重新抛出其次如果不是网络差导致的// 错误捕获后也要重新抛出。void_SeedMsg(conststrings){if(rand()%20){throwHttpException(网络不稳定发送失败,102,put);}elseif(rand()%70){throwHttpException(你已经不是对象的好友发送失败,103,put);}else{cout发送成功endl;}}voidSendMsg(conststrings){//发送消息失败则再重试3次for(size_t i0;i4;i){try{_SeedMsg(s);break;}catch(constExceptione){//捕获异常if中是102网络不稳定//捕获异常else中不是102号错误则将异常重新抛出if(e.getid()102){//重试三次以后否失败了则说明网络太差了重新抛出异常if(i3)throw;cout开始第i1重试endl;}else{throw;}}}}intmain(){srand(time(0));string strxxxxxxxx;while(cinstr){//this_thread::sleep_for(chrono::seconds(1));try{SendMsg(str);}catch(constExceptione){coute.what()endlendl;}catch(...){coutUnkown Exceptionendl;}}return0;}在上面的代码中发送消息如果失败会重新发送三次消息如果三次发送都失败那么捕捉异常4、异常安全构造函数完成对象的构造和初始化最好不要在构造函数中抛出异常否则可能导致对象不完整或没有完全初始化其次析构函数中最好不要在析构函数中抛出异常如果抛出异常也要谨慎处理比如析构函数要释放10个资源释放到第5个时抛出异常则也需要捕获处理否则后面的5个资源就没释放也资源泄漏了5、异常规范对于用户和编译器而言预先知道某个程序会不会抛出异常会有很大帮助有助于简化调用函数的代码C98中函数参数列表的后面接throw()表示函数不抛异常函数参数列表的后面接throw(类型1,类型2…)表示可能会抛出多种类型的异常可能会抛出的类型用逗号分割// C98写法承诺本函数不抛异常intAdd(inta,intb)throw(){returnab;}// 声明本函数最多只抛string、int两种异常voidTest(intx)throw(std::string,int){if(x0)throwstd::string(除零错误);if(x0)throw-1;// 不能抛出string/int以外的类型否则运行时出错}C11后一个函数如果不抛异常就在函数参数列表后面加noexcept但是⼀个声明了noexcept的函数抛出了异常程序会调用terminate 终止程序noexcept(expression)还可以作为一个运算符去检测一个表达式是否会抛出异常可能会则返回false不会就返回true#includeiostream#includestringusingnamespacestd;// 标注noexcept承诺无异常voidSafeFunc()noexcept{cout安全函数不会报错endl;// 下面放开注释运行直接崩溃终止程序// throw string(我违规抛异常了);}intmain(){try{SafeFunc();}catch(...){cout捕获异常endl;}return0;}正常运行输出安全函数不会报错如果取消throw注释不会进入 catch程序直接闪退终止6、异常的优缺点6.1 优点异常对象定义好了相比错误码的方式可以清晰准确的展示出错误的各种信息甚至可以包含堆栈调用的信息这样可以帮助更好的定位程序的 bug部分函数使用异常更好处理比如构造函数没有返回值不方便使用错误码方式处理。比如T operator[]这样的函数如果 pos 越界了只能使用异常或者终止程序处理没办法通过返回值表示错误很多的第三方库都包含异常比如boost、gtest、gmock等等常用的库那么我们使用它们也需要使用异常6.2 缺点异常会导致程序的执行流乱跳并且非常的混乱并且是运行时出错抛异常就会乱跳。这会导致我们跟踪调试时以及分析程序时比较困难异常会有一些性能的开销。当然在现代硬件速度很快的情况下这个影响基本忽略不计C没有垃圾回收机制资源需要自己管理。有了异常非常容易导致内存泄漏、死锁等异常安全问题。这个需要使用RAII来处理资源的管理问题。学习成本较高C标准库的异常体系定义得不好导致大家各自定义各自的异常体系非常的混乱