异常
Last updated on 3 months ago
异常
程序几乎不可避免地会出现异常,就比如你写了一个除法程序,保不齐会有人手贱非得去试一下分母为 0 会发生什么事情。因此应当异常就显得尤为重要。
异常应当的方式有三种,前两种分别是:调用 abort
强行退出、返回错误码。这两个,emmmm,说实话,没啥技术含量,我们跳过。
我们着重看通过异常机制来处理异常
异常机制
异常提供了将控制权从程序的一个部分传递到另一个部分的途径。C++
对于异常的处理由 try
块和 catch
块组成。
try
块用于表示下面这段代码可能会引发异常,通常在后面会跟一个throw
语句(也可以没有)。- 如果
try
块当中引发了异常,则会直接跳转到后面的catch
块。如果没有引发异常,则会直接跳过后面所有的catch
块
- 如果
throw
语句是跳转的意思,命令程序跳转到另一条语句。throw
后面的类型(字符串类型或者类类型)指出的是异常的特征。- 我们这里解释一下跳转的含义:执行
throw
语句类似于执行返回语句,但并不是将控制流返回到调用函数,而是沿着函数的调用序列后退,直到找到包含try
块的函数。
- 我们这里解释一下跳转的含义:执行
catch
块是对异常处理的程序,后面位于括号内的是类型声明,这个表示需要用哪一个catch
块来处理这个异常。即throw
抛出的值会有catch
后面对应的类型接住。如果catch
后面的类型声明不知道写什么,可以写catch(...)
这表示任何类型都由这个部分来处理。
将对象用作异常类型
我们可以在 throw
后面的语句内写上一个自定义类型,这么做的好处是显然的,我们可以很方便的知道该异常所代表的含义。
需要注意的是,如果是将对象作为异常类型,在
catch
块捕获的时候一定要写上引用。具体的例子看下面的代码:
1 |
|
你肯定会说,既然是一个副本,那我用引用指向它有什么用呢,引用本身就是为了不希望创建副本。
实际上这里用引用的考量并不是在这里,引用还有一个重要用途是:基类指针可以指向派生类对象。所以,如果我们有一个由继承所组成的异常类体系,只需要一个基类引用就可以指向所有的派生类对象,这不是很方便吗?
我们来看这个例子:
1 |
|
需要注意的是,引发异常的对象将被第一个与之匹配的
catch
块捕获,因此后面 catch
块中的顺序需要与继承的顺序相反。即捕获位于层次结构最下面的异常类的
catch
块将放在最上面,捕获基类异常的 catch
块将放在在下面。
异常规范和 C++ 11
有时候,一项理论看似很有前途,但在实际应用当中往往不是这样,到最后就慢慢地被摒弃了。这指的就是异常规范。异常规范是在 C++98 被加入到标准当中,但在 C++11 却被摒弃了。这意味着这项功能目前仍可能处在标准当中但未来可能会被废除,不建议使用。
我们直接举两个例子说明这个吧:
1 |
|
这样看的话你会觉得这东西还有点用,但下面我要说这东西的定义是什么,看完之后。。。。我对这东西的评价是:早点废除吧。
异常规范的作用之一是,告诉用户需要使用 try
块,也就是说这个函数可能引发异常,对应于第二个例子。但这个功能吧。。。。你直接用注释不好吗?
异常规范的另一个作用是,让编译器添加程序在运行阶段检查的代码,确定该函数是否违反了异常规范。但这个功能,你想啊,你写的这个函数它没有报错,但这个函数调用的另一个函数报错了,这样就很难检查了。
当然,C++11 支持另一种异常规范,可以用 noexcept
关键字来指出函数不会引发异常(这东西是关键字),这就跟上面的第一点一样。
1 |
|
栈解退
假设 try
块没有直接调用引发异常的函数,而是调用了对引发异常的函数进行调用的函数,则程序控制流就会从引发异常的函数开始回退,直到找到包含
try
块和处理模块的函数为止,这一过程我们称之为栈解退。
当涉及到函数调用时,编译器会为被调用函数在栈中为其开辟一块地址,并将这块地址的起始地址保存在调用函数中。之后,程序控制流将会跳转到被调用函数,往后会对该函数进行执行。如此这般,便是函数嵌套调用的底层实现。
当被调用函数执行完毕后,会释放所有的自动变量,控制流将自动回到调用函数,继续执行。
假设被调用函数引发了异常,那么该函数在栈中的空间将会直接被释放,不会执行引发异常后面的代码,多重嵌套调用也是同理。编译器会将该异常一直回退,直到找到包含
try
块和 catch
块为止。
上面的「直接被释放」的意思是,自动变量会直接释放,类对象会调用其析构函数。
exception 类
exception
头文件定义了 exception
类,代码可以引发 exception
异常,也可以将
exception
类用作基类。
exception
类的底层实现是一个 string
类型的对象,因此可以直接用 string
对象来对其进行初始化。除此之外,exception
类中还包含一个虚函数成员 what
,它会返回一个字符串,其具体的值随实现而异。
C++ 定义了很多基于 exception
的异常类型,我们这里简单说明一下,使用起来很简单。
stdexcept 异常类
头文件 stdexcept
定义了两个异常类:logic_error
和 runtime_error
,它们均是 exception
类派生而来。
logic_error
派生了如下类:
- domain_error ,定义域(domain)错误,
- invalid_error ,非法参数,给函数传递了一个意料之外的参数
- length_error ,没有足够的空间来执行所需的操作
- out_of_bounds ,索引错误,通常用在数组下标
runtime_error
派生了如下类:
- range_error ,值域(range)错误
- overflow_error
- underflow_error
一般来说浮点数会有一个可以表示的最小非零值,当计算结果比这个值还小的时候会引发下溢(underflow)错误。
同理,浮点数有一个所能表示的最大量级,当计算结果大于这个数时,会发生上溢(overflow)错误。
当计算结果不在函数所能允许的范围内,但没有发生上溢和下溢错误,则可以用
range_error
bad_alloc 和 new
对于 new
引发的申请空间所导致的异常,C++
最新的处理方法是让 new
抛出一个 bad_alloc
异常,如:
1 |
|
原先的处理方式是 new
在申请失败时返回空指针。处于兼容性的需要,编译器提供了一个标记让用户自己选择所需要的行为:
1 |
|
这样就不能用异常处理程序了。
异常导致的问题
异常被引发后,在两种情况下会导致问题。如果异常是由带有异常规范的函数所引发的,则它引发的异常必须与异常规范列表中的某个异常相匹配(在继承体系中,派生类异常可以与基类异常相匹配),否则称为异常意外。默认情况下会导致程序终止。
如果异常不是在函数中引发的,必须捕获它。如果没有捕获(没有与异常匹配的
catch
块时),则该异常称为未捕获异常。默认情况下,这将导致程序终止。
实际上,异常意外与未捕获异常所导致的行为是可以修改的,但我们这里对此不过多赘述,直接去 C++ Primer Plus 的 517 页看就行。