异常

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class bad_cal
{
private:
int a, b;
public:
bad_cal(int e1 = 0, int e2 = 0)
:a(e1), b(e2)
{ }
void mesg() { cout << "error" << endl; }
};

int cal(int n, int m)
{
if (n != m)
return n + m;
throw bad_cal(n, m);//引发异常时编译器会创建一个临时拷贝,就算下面指定的是引用
}

int main()
{
int n, m;
while (cin >> n >> m)
{
try
{
cout << cal(n, m) << endl;
}
catch (bad_cal& bc)//这会指向一个编译器创建的临时副本,并不会指向原对象
{
bc.mesg();
}
}

return 0;
}

你肯定会说,既然是一个副本,那我用引用指向它有什么用呢,引用本身就是为了不希望创建副本。

实际上这里用引用的考量并不是在这里,引用还有一个重要用途是:基类指针可以指向派生类对象。所以,如果我们有一个由继承所组成的异常类体系,只需要一个基类引用就可以指向所有的派生类对象,这不是很方便吗?

我们来看这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class bad1 {}
class bad2 : public bad1 {}
class bad3 : public bad2 {}
void fun()
{
try
{
if()
throw bad1;
else if()
throw bad2;
else throw bad3;
}
catch(bad3) { }
catch(bad2) { }
catch(bad1) { }
}

需要注意的是,引发异常的对象将被第一个与之匹配的 catch 块捕获,因此后面 catch 块中的顺序需要与继承的顺序相反。即捕获位于层次结构最下面的异常类的 catch 块将放在最上面,捕获基类异常的 catch 块将放在在下面。

异常规范和 C++ 11

有时候,一项理论看似很有前途,但在实际应用当中往往不是这样,到最后就慢慢地被摒弃了。这指的就是异常规范。异常规范是在 C++98 被加入到标准当中,但在 C++11 却被摒弃了。这意味着这项功能目前仍可能处在标准当中但未来可能会被废除,不建议使用。

我们直接举两个例子说明这个吧:

1
2
3
//函数声明如下
void cal() throw(); //说明该函数不会抛出异常
void cla() throw(bad_cal);//说明该函数可能会抛出 bad_cal 类型的异常

这样看的话你会觉得这东西还有点用,但下面我要说这东西的定义是什么,看完之后。。。。我对这东西的评价是:早点废除吧。

异常规范的作用之一是,告诉用户需要使用 try 块,也就是说这个函数可能引发异常,对应于第二个例子。但这个功能吧。。。。你直接用注释不好吗?

异常规范的另一个作用是,让编译器添加程序在运行阶段检查的代码,确定该函数是否违反了异常规范。但这个功能,你想啊,你写的这个函数它没有报错,但这个函数调用的另一个函数报错了,这样就很难检查了。

当然,C++11 支持另一种异常规范,可以noexcept 关键字来指出函数不会引发异常(这东西是关键字),这就跟上面的第一点一样。

1
void cal() noexcept;

栈解退

假设 try 块没有直接调用引发异常的函数,而是调用了对引发异常的函数进行调用的函数,则程序控制流就会从引发异常的函数开始回退,直到找到包含 try 块和处理模块的函数为止,这一过程我们称之为栈解退。

当涉及到函数调用时,编译器会为被调用函数在栈中为其开辟一块地址,并将这块地址的起始地址保存在调用函数中。之后,程序控制流将会跳转到被调用函数,往后会对该函数进行执行。如此这般,便是函数嵌套调用的底层实现。

当被调用函数执行完毕后,会释放所有的自动变量,控制流将自动回到调用函数,继续执行。

假设被调用函数引发了异常,那么该函数在栈中的空间将会直接被释放,不会执行引发异常后面的代码,多重嵌套调用也是同理。编译器会将该异常一直回退,直到找到包含 try 块和 catch 块为止。

上面的「直接被释放」的意思是,自动变量会直接释放,类对象会调用其析构函数。

exception 类

exception 头文件定义了 exception 类,代码可以引发 exception 异常,也可以将 exception 类用作基类。

exception 类的底层实现是一个 string 类型的对象,因此可以直接用 string 对象来对其进行初始化。除此之外,exception 类中还包含一个虚函数成员 what ,它会返回一个字符串,其具体的值随实现而异。

C++ 定义了很多基于 exception 的异常类型,我们这里简单说明一下,使用起来很简单。

stdexcept 异常类

头文件 stdexcept 定义了两个异常类:logic_errorruntime_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
2
3
4
5
6
7
8
try
{
auto pt = new somthing
}
catch(bad_alloc & r)
{
...
}

原先的处理方式是 new 在申请失败时返回空指针。处于兼容性的需要,编译器提供了一个标记让用户自己选择所需要的行为:

1
2
3
4
5
auto pt = new (std::nothrow) somthing
if(pt == 0)
{
...
}

这样就不能用异常处理程序了。

异常导致的问题

异常被引发后,在两种情况下会导致问题。如果异常是由带有异常规范的函数所引发的,则它引发的异常必须与异常规范列表中的某个异常相匹配(在继承体系中,派生类异常可以与基类异常相匹配),否则称为异常意外。默认情况下会导致程序终止。

如果异常不是在函数中引发的,必须捕获它。如果没有捕获(没有与异常匹配的 catch 块时),则该异常称为未捕获异常。默认情况下,这将导致程序终止。

实际上,异常意外与未捕获异常所导致的行为是可以修改的,但我们这里对此不过多赘述,直接去 C++ Primer Plus 的 517 页看就行。


异常
https://nishikichisato.github.io/2022/08/31/C++ Primer Plus/异常/
Author
Nishiki Chisato
Posted on
August 31, 2022
Updated on
November 25, 2023
Licensed under