在C语言中我们,程序运行结果最终只有,三种状态:结果对,结果不对,异常终止。
其中处理错误的方式就是依据错误码,错误码一般又被赋予了各自的意义,依据错误
码来进一步修改代码。知道错误码意义的人有可能查阅记录来判断是出了哪种,有点
浪费时间,而不知道的人,完全无从下手,错误码的存在也就毫无意义了。而在OO语
言中,有着抛异常的概念,它能够比错误码更加精确的指出问题的所在,而C++作为OO
语言的其中一员,也具有抛异常功能,今天让我来介绍C++中的异常。
导致程序异常终止比较经典一个就是除零,C语言中一般是使用assert来处理,让我们来看一个抛出和捕获异常的代码:
这就是一个简单的抛出异常捕获异常的代码,其中有需要注意的几点:
throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。
catch: 在您想要处理问题的地方,通过异常处理程序捕获异常.catch 关键字用于捕
获异常,可以有多个catch进行捕获。
try: try 块中的代码标识将被激活的特定异常,它后面通常跟着一个或多个 catch 块。
抛异常就是抛出一个对象,当使用catch接收的时候,catch接收对象跟抛出对象类型必须是一致的:
可以看到,就连const char*和string之间也不行。
并且,这个对象是当抛出后会生成一个临时对象,这个对象被catch后就会销毁。
类型严格匹配也有例外,那就是具有父子类的类型也可以抛出(派生类)和接收(基类)。
使用catch(…)可以接受任意类型的异常,但是很明显你无法得知这个抛出的异常带有什么信息:
当抛出异常之后,会匹配最近的可匹配的异常捕获模块:
如果不匹配会一直沿着栈帧找。如果找到main函数中都没有,程序就终止了。
并且异常地抛出和接收具有跳跃性,也就是当有抛异常之后有匹配的catch时,代码会直接跳到catch执行后续代码:
可以看到,并没有释放arr的内存,那这样就会导致内存泄漏,C++中异常经常会导致内存泄漏。所以就会有解决方式,那就是异常的重新抛出:
说白了就是,在栈帧链中开辟资源的栈帧中,再次接收和抛出异常,在抛出异常的代码中释放该栈帧中前面开辟的资源,接受的时候使用catch(…)然后抛出的时候时throw;意思就是我接受了一个异常,我也不知道是什么,然后再把他继续抛出就可以。算是特殊处理。但是这种方式有着很明显的缺点,那就是每有资源在栈帧中需要释放时都需要写这么一变,太麻烦了,那还有另一种解决方法,那就是RAII的思想,在C++中那就是智能指针。这个后面会介绍。
构造函数完成对象的构造和初始化,最好不要在构造函数中抛出异常,否则可能导致对
象不完整或没有完全初始化
析构函数主要完成资源的清理,最好不要在析构函数内抛出异常,否则可能导致资源泄
漏(内存泄漏、句柄未关闭等)
C++中异常经常会导致资源泄漏的问题,比如在new和delete中抛出了异常,导致内存
泄漏,在lock和unlock之间抛出了异常导致死锁,C++经常使用RAII来解决以上问题。
为了能够让自己写的代码可读性很高,C++标准提出了一种规范的写法:
这样能够显示出我这个函数中可能会抛出这样的异常,至于为什么是可能,代码展示:
可以看到我这里,显示的意思是可能会抛出int类型的异常,但是我函数中并没有抛出int异常的部分,我的代码依旧正常正常运行。
我没有表示这个函数里可能会抛出string的异常,但是代码依旧正常运行,所以,这个规范表示也只是能提高代码的可读性,并不会代码的安全性(只是一个提示)。
那基于这种可能会出现摆设的情况,那么C++11中就提出了一个关键noexcept,这个关键字说明该函数中不会抛出异常。
那么可以看到,这个关键字会起作用,函数中就算真的抛出了异常,后面也不会接收。所以要注意。
我们上面说了,关于抛出异常的类型和接收的类型可以不是严格类型一致的,那就是具有父子关系的类型,而在公司中的工程中,一般都会有一套自定义的异常体系:
那很明显了,需要实现这种体系,必须得实现多态。
out_of_range也就是我们的访问内存越界了:
我们也可以用一下:
new和delete出错的话也是以抛异常的方式来处理的。
1. 异常对象定义好了,相比错误码的方式可以清晰准确的展示出错误的各种信息,甚
至可以包含堆栈调用的信息,这样可以帮助更好的定位程序的bug。
2.返回错误码的传统方式有个很大的问题就是,在函数调用链中,深层的函数返回了
错误,那么我们得层层返回错误,最外层才能拿到错误。
3. 很多的第三方库都包含异常,比如boost、gtest、gmock等等常用的库,那么我们
使用它们也需要使用异常。
4. 部分函数使用异常更好处理,比如构造函数没有返回值,不方便使用错误码方式处理。
比如T& operator这样的函数,如果pos越界了只能使用异常或者终止程序处理,没办法
通过返回值表示错误。
1.异常会导致程序的执行流乱跳,并且非常的混乱,并且是运行时出错抛
异常就会乱跳。这会 导致我们跟踪调试时以及分析程序时,比较困难。
2. 异常会有一些性能的开销。当然在现代硬件速度很快的情况下,这个
影响基本忽略不计。
3. C++没有垃圾回收机制,资源需要自己管理。有了异常非常容易导致内
存泄漏、死锁等异常 安全问题。这个需要使用RAII来处理资源的管理问
题。学习成本较高。
4. C++标准库的异常体系定义得不好,导致大家各自定义各自的异常体系,
非常的混乱。
5. 异常尽量规范使用,否则后果不堪设想,随意抛异常,外层捕获的用户
苦不堪言。
所以异常 规范有两点:
一、抛出异常类型都继承自一个基类。
二、函数是否抛异常、抛什么异常,都 使用 func() throw();的方式规范
化。
异常总体而言,利大于弊,所以工程中我们还是鼓励使用异常的。另外OO的语言基本都是用异常处理错误,这也可以看出这是大势所趋。