程序运行时经常会碰到一些异常情况,例如,做除法的时候除数为0;用户输入年龄的时候输入了一个负数;用new运算符动态分配空间的时候空间不够了导致无法分配;访问数组元素的时候下标越界了;要打开文件读取的时候文件却不存在,等等。对这些异常情况如果不能发现并加以处理,很可能导致程序崩溃。所谓 处理可以是给出错误提示信息,然后让程序沿一条不会出错的路径继续执行;也可能是不得不结束程序,但在结束前做一些必要的工作,如将内存中的数据写到文件、关闭打开的文件、释放动态分配的内存空间等。

如果一旦发现异常情况就立即处理,未必妥当,因为在一个函数执行的过程中发生的异常,有的情况下由该函数的调用者来决定如何处理更合适。尤其像库函数这样提供给程序员调用,用以完成与具体应用无关的通用功能的函数,执行过程中对异常贸然进行处理,未必会符合调用它的程序的需要。此外,将异常分散在各处处理不利于代码的维护,尤其是对不同地方发生的同一种异常,都要编写相同的处理代码,也是不必要的重复和冗余。如果能够在发生各种异常时,让程序都走到同一个地方,这个地方能够对异常进行集中处理,则程序就会容易编写、容易维护得多。

鉴于上述原因,C++引入了 异常处理机制。其基本思想就是:函数A在执行过程中发现异常时,可以不加处理,而只是“抛出一个异常”给A的调用者,假定成为函数B。抛出异常而不加处理会导致函数A立即终止,此种情况下,函数B可以选择捕获A抛出的异常进行处理,也可以选择置之不理。如果置之不理,这个异常就会被抛给B的调用者;如果一层层的函数都不处理异常,最终异常会被抛给最外层的main函数。main函数应该处理异常。如果main函数也不处理异常,那么程序就会立即异常的中止。

C++异常处理基本语法

C++通过 throw语句和 try...catch语句实现对异常的处理。

throw语句的语法如下

1
throw 表达式;

该语句抛出一个异常。异常是一个表达式,其值的类型可以是基本类型,也可以是类。

try...catch语句的语法如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
try {
	语句组
}
catch(异常类型) {
	异常处理代码
}
...
catch(异常类型) {
	异常处理代码
}

catch可以有多个,至少一个。

try...catch语句执行的过程是:执行 try块的内容,如果执行的过程中没有异常抛出,那么执行完后就执行最后一个 catch块后面的语句,所有 catch块里面的语句都不会被执行。如果 try块执行的过程中抛出了异常,那么抛出异常后立即跳转到第一个 异常类型和抛出的异常类型匹配的 catch块里面去执行(称为异常被该catch块 捕获),执行完后在跳到最后一个catch块后面继续执行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
using namespace std;

int main()
{
	double m, n;
	cin >> m >> n;
	try {
		cout << "before dividing." << endl;
		if (n == 0)
			throw -1;	//抛出int类型异常
		else
			cout << m / n << endl;
		cout << "after dividing." << endl;
	}
	catch(double d) {
		cout << "catch(doubel) " << d << endl;
	}
	catch(int e) {
		cout << "catch(int) " << e << endl;
	}
	cout << "finished." << endl;
}

能够捕获任何异常的catch语句

如果希望不论抛出那种类型的异常都能捕获,可以编写如下catch块

1
2
3
catch(...) {
	...
}

这样的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
#include <iostream>
using namespace std;

int main()
{
	double m, n;
	cin >> m >> n;
	try {
		cout << "before dividing." << endl;
		if (n == 0)
			throw -1;	//抛出int类型异常
		else if (m == 0)
			throw -1.0;	//抛出double异常
		else
			cout << m / n << endl;
		cout << "after dividing." << endl;
	}
	catch(double d) {
		cout << "catch(doubel) " << d << endl;
	}
	catch(...) {
		cout << "catch(...) " << endl;
	}
	cout << "finished." << endl;
}

异常的再抛出

如果一个函数在执行的过程中,抛出的异常在本函数内就被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
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <iostream>
#include <string>
using namespace std;

class CException
{
public:
	string msg;
	CException(string s):msg(s) {}
};

double Divide(double x, double y)
{
	if (y == 0)
		throw CException("divided by zero");
	cout << "in Divide" << endl;
	return x / y;
}

int CountTax(int salary)
{
	try {
		if (salary < 0)
			throw -1;
		cout << "counting -1" << endl;
	}
	catch (int) {
		cout << "salary < 0" << endl;
	}
	cout << "tax counted" << endl;
	return salary * 0.15;
}

int main()
{
	double f = 1.2;
	try {
		CountTax(-1);
		f = Divide(3, 0);
		cout << "end of try block" << endl;
	}
	catch(CException e) {
		cout << e.msg << endl;
	}
	cout << "f = " << f << endl;
	cout << "finished" << endl;
	return 0;
}

虽然函数也可以通过返回值或者传引用的参数通知调用者发生了异常,但采用这种方式的话,每次调用该函数都要去判断是否发生异常,在多处处理调用该函数的时候比较麻烦。有了异常处理机制,可以将这多出调用都写在一个try块里面,任何一处调用发生异常都会被匹配的catch块捕获并处理,就不需要每次调用完都判断是否发生异常了。

有时虽然在函数中对异常进行了处理,但是还是希望能够通知调用者,以便让调用者知道发生了异常,可以做进一步处理。在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
#include <iostream>
#include <string>
using namespace std;

int CountTax(int salary)
{
	try {
		if (salary < 0)
			throw string("zero salary");
		cout << "counting tax" << endl;
	}
	catch (string s) {
		cout << "CountTax error: " << s << endl;
		throw;			//继续抛出捕获
	}
	cout << "tax counted" << endl;
	return salary * 0.15;
}

int main()
{
	double f = 1.2;
	try {
		CountTax(-1);
		cout << "end of try block" << endl;
	}
	catch(string s) {
		cout << e << endl;
	}
	cout << "finished" << endl;
	return 0;
}

函数的异常声明列表

为了增强程序的可读性和可维护性,C++允许在函数声明和定义时加上它所能抛出的异常的列表。

1
2
3
4
void func() throw(int, double, A, B, C);

//或者
void func() throw(int, double, A, B, C) {...}

如果异常声明列表编写为

1
void func() throw();

则说明func函数不会抛出任何异常。

一个函数如果不交代抛出哪些类型的异常,就可以抛出任何类型的异常。

C++标准异常类

C++标准库中有一些类代表异常,这些类都是从 exception类派生而来的。常用的几个类如下:

  • bad_typeid
  • bad_cast
  • bad_alloc
  • ios_base::failure
  • out_of_range

C++程序在碰到这些异常时,即使程序中没有编写throw语句,也会自动抛出上述异常类的对象。