【C++】异常处理
本篇博客让我们来认识一下C++中对于异常的处理机制
[TOC]
1.概念
1.1 C语言对于异常的处理
在之前我们遇到一些bug的时候,通常会用if
判断或者assert
断言等问题进行处理。但这种方式太过暴力,会直接中断程序的运行
另外一种办法是返回错误码,C语言的报错大多使用这种方式。不过这需要程序的用户自己去查对应的错误码表格,较为麻烦
1.2 C++异常
所谓异常,便是程序运行过程中可能遇到的bug或者问题。程序可以有选择地抛出一个异常,告知用户程序运行出现了问题。
C++标准库中便使用了一个exception
类来进行异常的处理,我们运行程序中遇到的一些报错,其实就是标准库里面抛出了对应的异常
其操作主要借助下面三个关键字
- throw 在出现问题的地方抛出异常
- try 监控后续代码中出现的异常,后续需要以catch作为结尾
- catch 用于捕获异常,同一个try可以用多个不同类型的catch进行捕获
throw关键字可以抛出任意类型
的异常
2.基本操作
下面用除法函数,以除0的情况来做一个最简单的演式
1 | int Div(){ |
2.1 需要注意的点
这里有几个需要注意的点:
catch类型对应
当我们进行抛异常的时候,一定需要有对应类型的catch,否则会报错
比如我们throw
的是一个常量字符串,如果用string
来catch,就会因为类型不匹配而出现报错
所以当我们使用某一个会抛异常的函数的时候,一定要注意其抛出异常的类型
利用…进行全捕获
假设我们不知道这里面会抛出什么类型的错误呢?总不能把所有类型都catch一下吧?
当然不需要,我们可以使用下面的函数进行全捕获
这就可以用于当我们不知道报错类型的时候。不过一般的使用场景是,在这之前先catch
已知的错误类型,最后再加上一个全捕或,作为未知错误
的标识
不过catch(...)
有一个缺点,那便是我们不能知道异常的类型
基类捕获派生类的异常
当我们出现异常的时候,如果throw
了一个子类对象,可以用基类的引用来接收!
1 | class A { |
这个在进行继承多态的错误编写的时候就很有用啦
2.2 异常和栈帧
抛出异常后,会被离这个异常最近的catch捕获,如果没有任何catch则会报错
比如我们单独写一个函数,而这个函数体内有try/catch
的话,那么会直接和这个最近的匹配,并不会和main函数里面的匹配
而如果该函数里面没有进行此操作,则会直接到main的对应catch处
注意!这里是直接跳转到对应catch语句,并不会出现先跳到testCatch
函数在跳回main的情况!
2.3 重新抛出异常
假设我们遇到了这种情况
1 | void testab() |
在testab()
函数中抛出了异常,导致testD()
函数提前终止!
可我们new的东西还没释放呢!
这就出现了内存泄漏!
注意:内存泄漏是一个不能被忽略的问题,即便我们每一次
new
的空间很小,但是积小成多就是大问题!
这时候我们就需要提前进行异常处理,如果出现问题,先释放我们new的资源之后,再将异常重新抛出。可以理解为是提前拦截
异常
3.异常需要注意的一些问题
3.1 异常安全
当我们操作异常的时候,需要注意一些相关的问题
- 上面
2.3
中提到的内存泄露问题,在new和delete之间抛出异常而没有中途处理,导致内存泄漏 - 不要在构造或析构函数中抛出异常,否则可能导致对象不完全初始化(对象不完整)或不完全析构(内存泄漏)
- 多线程操作中在lock与unlock之间抛出异常,导致死锁
当然是有解决方案的,C++使用RAII
来解决上述问题,这个待我下一篇智能指针的博客来讲解!
3.2 异常规范
因为异常都是手动写代码进行处理的,那么就极其需要些代码的老哥拥有很好的编程规范。
- 在函数后加上
throw(类型A,类型B)
可以列出这个函数能抛出的所有异常类型
1 | void test() throw(string,vector<int>); |
- 如果只跟一个类型,代表该函数只会抛出一种类型的异常
1 | void* operator new(size_t size) throw (std::bad_alloc); |
- 如果跟的是
throw()
代表这个函数不会抛出异常
1 | void* test2(size_t sz, void* p) throw(); |
在C++11
中还新增了一个关键字noexcept
来标识不会抛出异常
1 | void* test2(size_t sz, void* p) noexcept; |
但是这些都依赖于用户的编程习惯,C++并没有强制用户一定要在函数尾部写上异常抛出的类型。而且也不会对写好的异常类型和实际抛出的异常类型进行检查。
假设在项目合作中,来了一个“实习生”
- 写了一个会抛出异常的函数,却没有标识该函数会抛异常,那下面调用该函数的地方没有进行异常处理,那不就蛋糕了,程序提前中止!
- 或者说是抛出异常的类型写错了,没有对应的异常处理语句,只能被
catch(...)
捕获 - 或者是明明不抛出异常非要写自己抛出,白写了异常处理
以上三种情况都是我们不期望遇到的,所以在写相关函数的时候,最好明确标识相关异常抛出的类型!
有些人可能会对noexcept关键字有误解,认为写了这个关键字函数就无法抛出异常了。其实并不然,这里的throw和noexcept关键字的标定你可以理解为是函数的一个特殊的注释,并没有实际作用。后面跟着noexcept的函数依然可以抛出异常!
1 |
|
上面这个代码的运行结果如下,依旧抛出了异常。但是因为noexcept和抛出异常的操作在变成规范中冲突了,g++在编译的时候会进行warning警告,并且会在该函数抛出异常时直接终止程序(因为你写出了一个不符合编程规范的代码)
1 | > g++ test.cpp -o test && ./test |
当你把test_exp
函数的noexcept关键字声明删除后,这个程序才能按照预期正常抛出异常并被catch捕获
1 | > g++ test.cpp -o test && ./test |
3.3 自定义异常类型
要是在协作中,不同用户抛出了太多不同类型的异常,那还怎么调用函数?
前面提到了,子类抛出的异常可以用基类接受。所以在项目中一般都是会定义一个继承的规范异常体系,用于处理不同的异常。
这样我们就只需要捕获一个基类对象,就能捕获到所有派生类的异常对象。
关于基类捕获子类异常的方法在2.1
中已经提及,这里不再演示;
3.4 C++标准库中的异常
在C++标准库中,异常是围绕下图组织的
cplusplus:https://legacy.cplusplus.com/reference/exception/exception/?kw=exception
标准异常类的成员:
- 在上述继承体系中,每个类都有提供了构造函数、复制构造函数、和赋值操作符重载。
- logic_error类及其子类、runtime_error类及其子类,它们的构造函数是接受一个
string
类型的形参,用于异常信息的描述 - 所有的异常类都有一个
what()
方法,返回const char*
类型描述异常信息
标准异常类的具体描述:
异常名称 | 描述 |
---|---|
exception | 所有标准异常类的父类 |
bad_alloc | 当operator new and operator new[],请求分配内存失败时 |
bad_exception | 这是个特殊的异常。如果函数的异常抛出列表里声明了bad_exception异常,而函数内部抛出了异常抛出列表中没有的异常,不论什么类型,都会被替换为bad_exception 类型 |
bad_typeid | 使用typeid操作符,操作一个NULL指针,而该指针是带有虚函数的类,这时抛出bad_typeid异常 |
bad_cast | 使用dynamic_cast转换引用失败的时候 |
ios_base::failure | io操作过程出现错误 |
logic_error | 逻辑错误,可以在运行前检测的错误 |
runtime_error | 运行时错误,仅在运行时才可以检测的错误 |
logic_error
的子类:
异常名称 | 描述 |
---|---|
length_error | 试图生成一个超出该类型最大长度的对象时,例如很长的string |
domain_error | 参数的值域错误,主要用在数学函数中。例如使用一个负值调用只能操作非负数的函数 |
out_of_range | 超出有效范围,vetor的at抛出了此异常 |
invalid_argument | 参数不合适。在标准库中,当利用string对象构造bitset时,而string中的字符不是’0’或’1’的时候,抛出该异常 |
future_error(C++11) | This class defines the type of objects thrown as exceptions to report invalid operations on future objects or other elements of the library that may access a future‘s shared state. |
runtime_error
的子类:
异常名称 | 描述 |
---|---|
range_error | 计算结果超出了有意义的值域范围 |
overflow_error | 算术计算上溢 |
underflow_error | 算术计算下溢 |
system_error(C++11) | 运行时从操作系统或其他具有关联error_code的低级应用程序接口引发的异常 |
1 | 以上部分C++标准库异常解释来自 |
4.异常优缺点
优点
- 异常对象定义完备之后,相比于错误码的方式,能让用户更加清楚的了解到自己遇到了什么类型的问题,更好定位程序的bug
- 函数错误码若遇到,需要层层向外返回;而异常则通过
catch
可以直接跳到对应异常处理位置,避免多重判断和返回; - 第三方库包含异常,我们在使用类似于
boost/gtest
等第三方库的时候也需要使用对应的异常处理 - 对于
T& operator[]
这种操作符重载,我们没办法很好地使用返回值来标识错误(因为不同类型的返回值不一样,没办法统一处理)这时候就可以用异常来抛出越界问题
缺点
- 异常可能会导致程序到处乱跳(因为会跳到最近的
catch
位置)给观察错误情况增添了一些难度,代码可读性变差; - 异常有一定性能开销(可忽略);
- 异常容易导致资源泄漏等等问题(内存泄漏);
- 异常依赖于用户编程规范,否则函数调用容易出现异常没有得到处理的问题;
结语
总体而言,异常处理利大于弊。很多语言都是用异常来处理错误的,比如python
。只要维持一个良好的编程习惯,在函数后声明会抛出的异常类型,针对性进行处理,还是很香的!