异常和智能指针

发布时间:2026/7/23 9:08:47
异常和智能指针 一、异常的概念异常处理机制允许程序中独⽴开发的部分能够在运⾏时就出现的问题进⾏通信并做出相应的处理异常使得我们能够将问题的检测与解决问题的过程分开程序的⼀部分负责检测问题的出现然后解决问题的任务传递给程序的另⼀部分检测环节⽆须知道问题的处理模块的所有细节。C语⾔主要通过错误码的形式处理错误错误码本质就是对错误信息进⾏分类编号拿到错误码以后还要去查询错误信息⽐较⿇烦。异常时抛出⼀个对象这个对象可以函数更全⾯的各种信息。还有C语言的assert(断言)也是如果断言的判断条件成立就报错,这个比错误码更不友好,它是会使程序直接终止的,可能是你的程序一****小块部分有问题,不影响正常使用。所以总体来看c的异常还是非常好用的1.1异常的语法异常是要抛出(throw)和捕获(catch),首先你是哪里的代码或函数需要在运行时捕获异常需要包括在try{}里面然后在try里面或者是被调用函数里面用throw抛异常,抛出的异常可以是对象,然后catch(),相当于一个函数,参数类型要和抛出的对象的类型匹配,但可以是非常量对象被常量参数接受,然后执行catch子句注意: try和catch是配套使用的,在同一个局部域,写了try就一定要写catch而且在当前函数栈帧里的catch不匹配当前抛出的对象类型,就会沿着调用链去寻找是否有匹配的catch 如果找到匹配的catch⼦句处理后catch⼦句代码会继续执⾏并且main函数后面写的代码还会正常运行.如果到达main函数依旧没有找到匹配的catch⼦句程序会调⽤标准库的terminate函数终止程序1.2异常的使用看下面简单的例子能更好理解,如下是除法运算的函数,如果b是0,也就是分母为0的话就抛出string匿名对象并且里面的信息是分母不能为0,当前函数中有try/catch语句,就会先看这里的catch的参数类型与抛出的异常是否匹配,这里是不匹配,然后销毁函数去调用它的函数里再找,f函数中没有,再去mian函数里找,最后找到了,在执行catch子句里的代码如果是在main函数还没找到又因为main后面还有其他要执行的函数不想终止,那就在添加个catch(…)里面三个点表示接受任意类型的对象,所以如果前面都没匹配一定会调用这个,那么后面的代码也会正常运行doubleDivide(doublea,doubleb){try{if(b0){throwstring(分母不能是0!);}else{returna/b;}}catch(interrid){couterridendl;}}voidf(){inta,b;cinab;coutDivide(a,b)endl;}intmain(){try{f();}catch(conststrings){coutsendl;}catch(...){cout未知的异常endl;}return0;}1.3查找匹配的处理代码⼀般情况下抛出对象和catch是类型完全匹配的如果有多个类型匹配的就选择离他位置更近的那个。但是也有⼀些例外允许从⾮常量向常量的类型转换也就是权限缩⼩允许数组转换成指向数组元素类型的指针函数被转换成指向函数的指针允许从派⽣类向基类类型的转换这个点⾮常实⽤实际中继承体系基本都是⽤这个⽅式设计的。因为在实际项目中有多个组去负责部分区块业务的代码,如果要写异常处理的话catch就需要写很多类型,就很繁琐,但在项目中一般会有异常写成基类,每个组负责的区块做派生类,catch子句就能用基类对象接受,再通过多态重写的虚函数就能知道是哪个模块出了问题下面是一些简单实现的例子,下面包括简单的sql模块,网络层接受请求、和缓存区读取请求,三大模块的简单实现classException{public:Exception(conststringerrmsg,intid):_errmsg(errmsg),_id(id){}virtualstringwhat()const{return_errmsg;}intgetid()const{return_id;}protected:string _errmsg;int_id;};classSqlException:publicException{public:SqlException(conststringerrmsg,intid,conststringsql):Exception(errmsg,id),_sql(sql){}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();}用rand()生成的随机数来模拟判断,这里的srand就是以当前时间生成随机值,this_thread::sleep_for(chrono::seconds(1));这个是执行完异常后停一秒再随机输入随机值去调用#includethreadintmain(){srand((unsigned)time(nullptr));while(1){try{this_thread::sleep_for(chrono::seconds(1));HttpServer();}catch(constExceptione){coute.what()endl;}catch(...){cout未知异常endl;}}这里只截取了一部分,因为是死循环输入,这里就能看到只要一个catch的子句捕获抛出的异常对象就行,上面显示有?是可能处理异常时的字符集有问题不影响1.4异常重新抛出我们如日常生活中给朋友发消息难免会发生网络不好的情况,我们发送信息其实就是向服务器发送了请求然后再通过服务器发给你朋友,但网络不好可能会发送失败,但它不是请求一次不行就失败而是很多次才发不出去,就像我们的微信发不出消息就会有红色感叹号,也有可鞥是你被你朋友删掉了.类比到这里就相当于有时catch到⼀个异常对象后需要对错误进⾏分类其中的某种异常错误需要进⾏特殊的处理其他错误则重新抛出异常给外层调⽤链处理。捕获异常后需要重新抛出,重新抛出可以省略对象直接throw; 就可以把捕获的对象直接抛出。,下⾯程序模拟展⽰了聊天时发送消息发送失败补货异常但是可能在电梯地下室等场景⼿机信号不好则需要多次尝试如果多次尝试都发送不出去则就需要捕获异常再重新抛出其次如果不是⽹络差导致的错误捕获后也要重新抛出。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;}}}}1.5异常安全问题只要抛出了异常,就会回调寻找catch子句,在寻找的过程中,当前函数没有catch子句的话会销毁掉释放资源,这样可能可能造成内存泄漏,如果函数体内有向堆上申请空间的话就释放不了所以我们需要先释放资源再重新抛出,后面可以通过智能指针章节讲的RAII⽅式解决这种问题是更好的。doubleDivide(doublea,doubleb){try{if(b0){throwstring(分母不能是0!);}else{returna/b;}}}voidf(){inta,b;cinab;vectorint*vnewvectorint();catch(...){delete[]v;coutdelete[]endl;throw;//重新抛出异常}intmain(){try{Divide(a,b);}catch(conststrings){coutsendl;}return0;}1.6异常的规范• 对于⽤⼾和编译器⽽⾔预先知道某个程序会不会抛出异常⼤有裨益知道某个函数是否会抛出异常有助于简化调⽤函数的代码。• C98中函数参数列表的后⾯接throw()表⽰函数不抛异常函数参数列表的后⾯接throw(类型1,类型2…)表⽰可能会抛出多种类型的异常可能会抛出的类型⽤逗号分割。• C98的⽅式这种⽅式过于复杂实践中并不好⽤C11中进⾏了简化函数参数列表后⾯加noexcept表⽰不会抛出异常啥都不加表⽰可能会抛出异常。• 编译器并不会在编译时检查noexcept也就是说如果⼀个函数⽤noexcept修饰了但是同时⼜包含了throw语句或者调⽤的函数可能会抛出异常编译器还是会顺利编译通过的(有些编译器可能会报个警告)。但是⼀个声明了noexcept的函数抛出了异常程序会调⽤• noexcept(expression)还可以作为⼀个运算符去检测⼀个表达式是否会抛出异常可能会则返回false不会就返回true。二、智能指针上面也提到了抛出异常后有些代码不会执行而导致内存泄漏,所以推出了智能指针来管理这样的情况2.1RAII和智能指针的设计思路RAII是ResourceAcquisitionIsInitialization的缩写他是⼀种管理资源的类的设计思想本质是⼀种利⽤对象⽣命周期来管理获取到的动态资源避免资源泄漏这⾥的资源可以是内存、⽂件指针、⽹络连接、互斥锁等等。RAII在获取资源时把资源委托给⼀个对象接着控制对资源的访问资源在对象的⽣命周期内始终保持有效最后在对象析构的时候释放资源这样保障了资源的正常释放避免资源泄漏问题。简单来说就是本质就是智能指针是个类,它是个模板能存任意参数类型的对象它能管理你动态开出来的资源,函数栈帧销毁会调用它的析构函数所以不会发生内存泄漏而且它里面还重载了很多运算符,也能取到里面的值,下面是简单的实现templateclassTclassSmartPtr{public:// RAIISmartPtr(T*ptr):_ptr(ptr){}~SmartPtr(){coutdelete[] _ptrendl;delete[]_ptr;}// 重载运算符模拟指针的⾏为⽅便访问资源Toperator*(){return*_ptr;}T*operator-(){return_ptr;}Toperator[](size_t i){return_ptr[i];}private:T*_ptr;};doubleDivide(doublea,doubleb){try{if(b0){throwstring(分母不能是0!);}else{returna/b;}}}voidf(){inta,b;cinab;SmartPtrvectorintvnewvectorint();}intmain(){try{Divide(a,b);}catch(conststrings){coutsendl;}return0;}如上图这样改就能函数栈帧销毁前先析构v就不会发生内存泄漏,我感觉就是利用了对象的生命周期是虽局部域一起销毁的而设计的,并且设计的运算符重载使它能和指针一样使用。后面的知识下一篇博客再深入讲库里的智能指针的使用以及底层实现