C++笔记-异常

发布于:2025-06-17 ⋅ 阅读:(14) ⋅ 点赞:(0)

1.异常的概念及使用

1.1异常的概念

异常处理机制允许程序中独⽴开发的部分能够在运⾏时就出现的问题进⾏通信并做出相应的处理,异常使得我们能够将问题的检测与解决问题的过程分开,程序的⼀部分负责检测问题的出现,然后解决问题的任务传递给程序的另⼀部分,检测环节⽆须知道问题的处理模块的所有细节。
C语⾔主要通过错误码的形式处理错误,错误码本质就是对错误信息进⾏分类编号,拿到错误码以
后还要去查询错误信息,⽐较⿇烦。异常时抛出⼀个对象,这个对象可以函数更全⾯的各种信息。
1.2异常的抛出和捕获
程序出现问题时,我们通过抛出(throw)⼀个对象来引发⼀个异常,该对象的类型以及当前的调⽤
链决定了应该由哪个catch的处理代码来处理该异常。
对于抛出的异常,我们要注意以下几点:
 
1.选中的处理代码是调⽤链中与 该对象类型匹配 且离抛出异常位置 最近 的那⼀个。根据抛出对象的
类型和内容,程序的抛出异常部分告知异常处理部分到底发⽣了什么错误。
2. 当throw执⾏时,throw后⾯的语句将不再被执⾏。程序的执⾏从throw位置跳到与之匹配的catch
模块,catch可能是同⼀函数中的⼀个局部的catch,也可能是调⽤链中另⼀个函数中的catch,控
制权从throw位置转移到了catch位置。这⾥还有两个重要的含义:1、沿着调⽤链的函数可能提早
退出。2、⼀旦程序开始执⾏异常处理程序,沿着调⽤链创建的对象都将销毁。
3. 抛出异常对象后,会⽣成⼀个异常对象的拷⻉,因为抛出的异常对象可能是⼀个局部对象,所以会 ⽣成⼀个拷⻉对象,这个拷⻉的对象会在catch⼦句后销毁。(这⾥的处理类似于函数的传值返
回)。
而寻找合适的catch模块的过程叫什么,又是怎么操作的呢?
1.3栈展开
这个过程就叫做栈展开, 抛出异常后,程序暂停当前函数的执⾏,开始寻找与之匹配的catch⼦句,⾸先检查throw本⾝是否 在try块内部,如果在则查找匹配的catch语句,如果有匹配的,则跳到catch的地⽅进⾏处理。
如果当前函数中没有try/catch⼦句,或者有try/catch⼦句但是类型不匹配,则退出当前函数,继续
在外层调⽤函数链中查找。
如果到达main函数,依旧没有找到匹配的catch⼦句,程序会调⽤标准库的 terminate 函数终⽌程序,果找到匹配的catch⼦句处理后,catch⼦句代码会继续执⾏。
我们用图来演示整个过程就如上面所示,只看这张图大家可能还是不太理解栈展开的过程,下面我们来看一个例子:
 
根据上面的例子,我来复述代码的整个过程来帮助大家更好地理解:
 
首先通过while循环调用Func函数,进入到Func函数中我们按要求输入两个值,进而去调用Divide函数,进入到Divide函数后判断b是否等于0,不等于0的情况这里就不说了,程序会正常运行,我们主要讲如果等于0的情况。
如果输入的b等于0,此时我们就会抛出一个string类型的异常s,然后后面的代码就不再执行,进而去判断抛出的异常是否在try/catch语句中,在这个例子中是在的,所以就会去判断catch语句中的参数类型是否匹配,我们从代码可以看出并不匹配,Divide中的catch是int类型。
那么接下来就会向下找,此时就会来到Func函数中来找有没有符合的catch,从代码可以看出,并没有,注意const char*和string不是一个类型,并且此时Divide函数的栈帧已经结束了。
接着往下找,来到了main函数中,根据代码我们找到了符合的catch,并将异常的内容输出出来。
当然,如果在Func函数中就找到了符合的catch,就会调用Func函数中的catch,这就是选择最近的方式。
而上面的过程是在编译阶段编译器就会完成的,在运行时,抛出异常后,我们调试就会发现代码会直接跳转到main函数的catch这里。
可以看出,我们第二个数据为0值就会报出异常,而在实际应用中我们就可以根据报的异常快速找到问题所在的地方,下面我们再来看如果一直没有找到会怎么样:
此时程序就会 调⽤标准库的 terminate 函数终⽌程序。
其实在我们日常的练习过程当中基本用不到异常,使用异常频繁的场景是在项目中,也就是我们以后的工作过程中,异常这部分就会经常使用,我们接着往下看。
1.4查找匹配的处理代码
一般情况下抛出对象和catch是类型完全匹配的,如果有多个类型匹配的,就选择离他位置更近的
那个。
但是也有⼀些例外,允许从⾮常量向常量的类型转换,也就是权限缩⼩;允许数组转换成指向数组
元素类型的指针,函数被转换成指向函数的指针;允许从派⽣类向基类类型的转换,这个点⾮常实
⽤,实际中继承体系基本都是⽤这个⽅式设计的。
上面几种我就不多说了,主要来说最后一种:
允许派生类向基类类型的转换这种方式是为了解决比如:我们有一个项目,每个人都负责不同的部分,每个部分会抛出不同类型的异常,如果我们不管的话,那到最后我们要处理抛出的异常就需要写出各种类型的catch来匹配,可能有人觉得没什么,但是一个项目动辄几千几万行代码甚至更多,抛出异常的类型太多了,你要一个一个写吗?
所以这对于这种情况,我们可以写一个异常的基类,里面写上虚函数或者纯虚函数,让抛出异常的类型来继承基类,这样每个人也可以在自己的类型中加入想加的内容,在抛出异常时我们可以利用多态写一个基类的对象即可去调用相应的catch。
如果到main函数,异常仍旧没有被匹配就会终⽌程序,不是发⽣严重错误的情况下,我们是不期望程序终⽌的,所以⼀般main函数中最后都会使⽤catch(...),它可以捕获任意类型的异常,但是不知道异常错误是什么。
我们来看一个例子来更好地理解这种方式:
以上面的为例,我列出了几种不同的会抛异常的类,sql就是数据库,cache就是缓存,http就是网络,从上面可以看出,这些类继承了基类后,依旧可以定义自己的成员变量。
我在基类中定义了纯虚函数,所以基类就变为抽象类,就无法实例化出对象,如果定义成虚函数,那么基类对象和派生类对象都可以被捕获,但是基类此时是抽象类,无法实例化出对象,所以只能捕获派生类对象,两种实现方式都可以。
注意这里是利用了多态的知识,派生类的对象可以赋值给基类对象,并去调用相应的重写虚函数。
此时我就可以根据随机生成的数来看都会抛出那些异常:
 
从上图我们可以看到,随机生成的数会抛出不同类型的异常,可能是sql的异常,可能是cache的异常,也可能是http的异常。
注意我上面要定义一个id的成员变量,我们要对抛出的异常进行编号,不然我们不好区分每种异常,在以后的应用中我们也会这样给异常进行编号。
1.5异常重新抛出
有时catch到⼀个异常对象后,需要对错误进⾏分类,其中的某种异常错误需要进⾏特殊的处理,其他错误则重新抛出异常给外层调⽤链处理。捕获异常后需要重新抛出,直接 throw; 就可以把捕获的对象直接抛出。
举个例子:我们的手机在某些地方,比如:电梯,地下室等地方信号会不好,此时我们如果给别人发消息,可能会在消息前面出现一个圆圈在转动,这种现象相信大家都遇到过,此时就是因为网络不好,抛出了异常,但不能因为信号不好就直接发不出去,因为信号不好不代表没有信号,所以底层在捕获了这种异常后,会多次尝试能否正常发送,不抛异常,如果在尝试多次后,依旧抛出异常,说明此时的网络确实没办法发送消息,在我们发送的消息面前就会出现红色感叹号。
而尝试多少次就和当前软件的底层有关,可能在设计时规定了重试3次,也可能是5次。
我们来模拟实现一下这种情况:
依旧利用上面的部分代码,通过程序我们会抛出“网络不稳定,发生失败”这个异常,此时按照我们上面的思路就会对抛出异常进行判断,如果不是我们要处理的异常,就接着抛出异常;如果是,我们就要进行多次尝试,所以在外面套了一层循环,在上面的代码中,我们重试3次,如果重试3次过后依旧抛出异常,那么就不再尝试,抛出异常。
我们利用随机数来模拟网络,当我们发送消息时根据随机数来决定我们的消息是否能发送成功。
注意:因为我上面的Exception类中实现的是纯虚函数,所以无法实例化出对象,所以只能throw;,而如果是虚函数,就可以throw e;
1.6异常安全问题
异常抛出后,后⾯的代码就不再执⾏,前⾯申请了资源(内存、锁等),后⾯进⾏释放,但是中间可
能会抛异常就会导致资源没有释放,这⾥由于异常就引发了资源泄漏,产⽣安全性的问题。中间我
们需要捕获异常,释放资源后⾯再重新抛出。
我们来看一个例子:
 
从上面的例子中我们可以看出如果b等于0,如果我们没有写能接收所有类型异常的代码来释放array,就会导致array数组的资源没有被释放,进而导致内存泄漏。
这种其实还好,我们再来看一种:
 
这种就会很麻烦,为什么这么说呢?
因为new也可能会抛出异常的,第一个new还好,不经还没有开辟空间,没有资源需要释放,但是第二个new如果过抛出异常了,那么你就要写try/catch语句来释放array的资源,以此类推如果过第三个new抛异常,也要写try/catch语句来释放array和array1的资源,不然就会导致内存泄漏,这才只有三个而已,所以下一章要讲的智能指针就是为了解决这种问题。
其次析构函数中,如果抛出异常也要谨慎处理,⽐如析构函数要释放10个资源,释放到第5个时抛
出异常,则也需要捕获处理,否则后⾯的5个资源就没释放,也内存泄漏了。
1.7异常规范
对于⽤⼾和编译器⽽⾔,预先知道某个程序会不会抛出异常⼤有裨益,知道某个函数是否会抛出异
常有助于简化调⽤函数的代码。
C++98中函数参数列表的后⾯接throw(),表⽰函数不抛异常,函数参数列表的后⾯接throw(类型1,
类型2...)表⽰可能会抛出多种类型的异常,可能会抛出的类型⽤逗号分割。
C++98的⽅式这种⽅式过于复杂,实践中并不好⽤,C++11中进⾏了简化,函数参数列表后⾯加
noexcept表⽰不会抛出异常,啥都不加表⽰可能会抛出异常。
比如:
编译器并不会在编译时检查noexcept,也就是说如果⼀个函数⽤noexcept修饰了,但是同时⼜包
含了throw语句或者调⽤的函数可能会抛出异常,编译器还是会顺利编译通过的(有些编译器可能会
报个警告)。但是⼀个声明了noexcept的函数抛出了异常,程序会调⽤ terminate 终⽌程序。
此时就是加了noexcept的同时又抛出了异常,就会直接终止程序。
noexcept(expression)还可以作为⼀个运算符去检测⼀个表达式是否会抛出异常,可能会则返回
false,不会就返回true。
而上面的例子中因为我在函数后加了noexcept的关键字,所以编译器就认为函数不会抛出异常,所以结果都为true,如果把noexcept去掉,我们再来看结果:
 
此时编译器就会认为可能会抛异常,所以此时的答案都为false。
2.标准库的异常
C++标准库也定义了⼀套⾃⼰的⼀套异常继承体系库,基类是exception,所以我们⽇常写程序,需要在主函数捕获exception即可,要获取异常信息,调⽤what函数,what是⼀个虚函数,派⽣类可以重写。
这就和我们上面的讲的基类和派生类的一样,我们用图来演示一下:
这里显示了一些我们常见的报错来自于那些异常来帮助我们更好地去理解。
以上就是异常的全部内容。

网站公告

今日签到

点亮在社区的每一天
去签到