文章目录
异常
异常的概念和基本语法
异常是一种处理错误的方式,当一个函数发现自己无法处理的错误时就可以抛出异常,让函数的直接或间接的调用者处理这个错误。
异常的三个关键字:
throw:
当问题出现时,根据情况的不同,通过throw抛出异常[异常可以是任意类型的变量
]
用throw抛出变量的时候,抛出的是它的拷贝,不是它的引用
catch:
在想要处理问题的地方,通过异常处理程序捕获异常。
catch 关键字用于捕获异常
可以有多个catch进行捕获
一个catch一般只捕获与它的()里面的类型匹配[必须严格匹配,不会隐式类型转换之后再匹配
]的异常
catch的只会捕获与它对应的try的{}中的throw抛出的异常
不过:
catch(...)可以捕获任意类型的异常,缺点是不知道捕获到的异常是什么类型的
try:
try {}里面的代码,可以直接或者间接[执行try {}里面的一个/多个函数的过程中,会遇到throw
]包含throw
它后面通常跟着一个或多个 catch 块
简单例举:
异常抛出和被接收的过程
当出现当前函数自己处理不了的问题时,throw抛出异常时:
首先检查throw自己是否在一个try{}, 如果是再查找与抛出的异常类型匹配的catch语句
如果有匹配的catch,则调到catch的地方进行处理。
处理以后,会继续执行catch子句之后的代码,继续运行程序没有匹配的catch或者抛出异常的throw没有被直接或间接的包含在某一个try的{}里面
则退出当前函数栈[类似于直接return,返回到上一层调用throw所处函数的函数栈桢中
]
返回到上一层函数的栈桢中之后,继续重复①和②的过程如果返回到了
main函数
的栈,在main函数
的栈桢中也还是没有匹配的catch或者抛出异常的throw没有被直接或间接的包含在某一个try的{}里面
此时没有上一层调用main的函数的栈桢了,只能终止程序
所以实际中,我们最后都要加一个catch(…)捕获任意类型的异常,否则当有异常没捕获,程序就会直接终止。
上述这个沿着调用链查找匹配的catch子句的①②③的过程称为栈展开
抽象图:
现实生活中,一般都会在main函数中捕获异常,进行异常的统一处理
而且一般抛出的异常都是自定义类型
这个自定义类型里面,一般包含错误信息和错误id[唯一标识某一个错误,可以更好地进行错误的匹配
]
接收错误的类型一般都是那个自定义类型的父类
异常的再次抛出
即把捕获到的异常,再次作为异常抛出
即:我尝试处理异常了,但我能力/权限不够,处理不了,要交给更nb的人
分两种情况
再次抛出被非catch(…)捕捉到的异常
语法:throw 明确的异常的名字
异常被非catch(…)的普通catch捕捉到了
因为普通的catch语句里面知道自己捕捉到了什么类型的异常
所以
当前的catch是有可能处理掉这个异常的
此时如果再次抛出这个被捕捉到的异常,一般是因为当前捕获了这个的异常的catch语句,解决不了这个异常
所以再把这个异常抛出去,尝试让更外层的catch语句捕获并处理
例
当发送消息的时候,因为网络不好,可能消息就发不出去
但是此时程序一般不会直接向用户,报出因网络错误消息发不出去
而是会再多尝试发送几次
此时第1次发送消息的时候,如果网络不好,就可以抛出一个网络不好的异常
然后由最接近它且类型匹配的catch捕获
然后在这个catch语句中尝试重新发送几次消息
如果尝试这几次还是发不出去
再把这个异常抛出去,给更外层的catch捕获,更外层的catch再向用户报出网络错误
再次抛出被catch(…)捕捉到的异常
语法:throw
[如果一个catch语句里面有throw,并且throw后面什么都没有加
那么就意味着那个catch里面的throw抛出的异常是catch捕获到的异常
]
异常被catch(…)捕捉到了
因为catch(…)语句里面根本不知道自己捕捉到了什么类型的异常
所以
catch(…)是根本不可能正常地处理掉
这个异常的
此时有两种情况:
①catch(…)位于一条异常处理流程的最末尾
,即它的存在就是为了防止程序因为异常终止的情况的
是用来捕捉其他catch处理不了的异常的(或者防止别人乱抛异常的)
此时catch(…)再次抛出捕捉到的异常也没用,因为再抛异常程序都终止了
此时一般的出来方法是在catch(…)内部,向用户报告错误了
②catch(…)没有位于一条异常处理流程的最末尾,而是中途
这种情况其实,就是只要try{}的代码出现了异常
不管是什么类型的异常,我都需要对他进行一些处理
处理完成之后
再把接收到的异常原封不动地抛出去
例
上图中,如果函数Division中抛出了异常
那么Division之后的代码是不会被执行的,所以array申请的资源就没有被释放
此时可以
但是每申请资源的地方都要这样的话,就太麻烦了
上面这样防止内存泄漏的,最好的方法是通过智能指针解决
异常规范
异常规范说明的目的是为了让函数使用者知道该函数可能抛出的异常有哪些。 可以在函数的后面接throw(类型),列出这个函数可能抛掷的所有异常类型。
函数的后面接throw(),表示函数不抛异常。
若无异常接口声明,则此函数可以抛掷任何类型的异常。
上图提到的一些异常规范
无论C++98还是C++11都是非强制性的
也就是你爱加不加
甚至有可能出现那个函数会抛异常,也加上noexcept[编译可以通过,但是运行的时候如果加了noexcept的函数抛出了异常,无论怎样都捕获不到,因为编译器默认他不会抛异常
]
或者是有可能会抛三个异常,但他只写了可能会抛两个异常
所以
除非有名文规定,否则这个不具有很强的参考性
异常安全
构造函数完成对象的构造和初始化,最好不要在构造函数中抛出异常,否则可能导致对象不完整或没有完全初始化
析构函数主要完成资源的清理,最好不要在析构函数内抛出异常,否则可能导致资源泄漏(内存泄漏、句柄未关闭等)
C++中异常经常会导致资源泄漏的问题,比如在new和delete中抛出了异常,导致内存泄漏,在lock和unlock之间抛出了异常导致死锁,C++经常使用RAII来解决以上问题,关于RAII之后智能指针会进行讲解
异常的优缺点
优点
异常对象定义好了,相比错误码的方式可以清晰准确的展示出错误的各种信息,甚至可以包含堆栈调用的信息,这样可以帮助更好的定位程序的bug
返回错误码的传统方式有个很大的问题就是,在函数调用链中,深层的函数返回了错误,那么我们得层层返回错误,最外层才能拿到错误,具体看下图:
很多的第三方库都包含异常,比如boost、gtest、gmock等等常用的库,那么我们使用它们也需要使用异常。
部分函数使用异常更好处理,比如构造函数没有返回值,不方便使用错误码方式处理
比如T& operator这样的函数,如果pos越界了只能使用异常或者终止程序处理,没办法通过返回值表示错误
缺点
异常会导致程序的执行流乱跳,并且非常的混乱,并且是运行时出错抛异常就会乱跳。这会导致我们跟踪调试时以及分析程序时,比较困难。
C++没有垃圾回收机制,资源需要自己管理。有了异常非常容易导致内存泄漏、死锁等异常安全问题。这个需要使用RAII来处理资源的管理问题。学习成本较高。
C++标准库的异常体系定义得不好,导致大家各自定义各自的异常体系,非常的混乱。
异常尽量规范使用,否则后果不堪设想,随意抛异常,外层捕获的用户苦不堪言。所以异常
规范有两点:
一、抛出异常类型都继承自一个基类。
二、函数是否抛异常、抛什么异常,都使用 func() throw();的方式规范化。
总结:
异常总体而言,利大于弊,所以工程中我们还是鼓励使用异常的。
所有高级语言基本都是用异常处理错误,这也可以看出这是大势所趋