[C++] 异常处理

异常是一种能够处理非正常行为的机制,也是异常产生端和异常处理端通信的一种方式。在软件开发时,通常的情况是,一方知道会发生何种异常但不知道如何处理,另一方不知道是否会发生异常,但在发生异常时它能够进行处理。所以,C++提供了异常处理机制。允许库的开发者在发生异常时抛出异常,而库的用户捕捉异常并进行合理的处理。

C++中与异常处理相关的关键字有:

throw:抛出异常。

try:测试某个程序块是否会抛出异常。

catch:对异常进行捕捉,然后处理。

因此,一般的异常处理程序的框架是:

库的开发者端:

void throw_test()
{
	if(something_is_bad) {
		throw exception;
	}
}

而库的用户端:

void catch_test()
{
	try {
		throw_test();
	} catch(exception & e) {
		// exception handler
	}
}

如上所示,在throw_test()中抛出一个exception异常,而在catch_test()中对throw_test()进行测试,如果发生了异常,就会被catch语句捕捉,并进行处理。


1 捕捉端是否要使用引用

读者看到上面捕捉异常时使用的是引用,那么,是否一定要使用引用呢?

一般传递参数的方式有三种:传递指针、传递值、传递引用。其实传递指针和传递值是类似的。

在捕捉异常时,如果采用指针传递,抛出异常后,就会脱离当前作用域,因此,就必须保证指针所指向的对象依然存在,可以有三种方式来保证:static对象、全局对象、堆上的对象。如果采用static和全局变量,这通常是不符合程序员的习惯,因为每次传递一个异常都必须声明一个static和全局变量,这当然不是良好的编程风格。如果采用堆上的对象,在抛出端进行new,那何时进行delete呢?用户必须保证在最后进行处理之后对对象进行delete,这对程序员产生了极大的负担。

在捕捉异常时,如果采用值传递,就必须产生两次拷贝,而且没有多态行为。具体的请看第2部分。

因此,在抛出对象时,应该尽量使用引用传递。采用引用传递没有上面的问题。


2 throw之后究竟发生了什么?

采用引用传递,通常在抛出端会有下面两种形式:

derived d;
throw d;

throw derived();

第一种方式是用的局部对象,第二中方式是用的临时对象。那么,离开当前作用域时,d会被析构,那么,它是如果保证d的对象能够传递到捕捉端呢?

当throw一个对象时,编译器会在堆上用抛出的对象复制构造一个临时的对象(G++的实现),因此,就算离开了当前作用域,在堆上同样有一个对象是抛出对象的副本。此时,根据捕捉端会有不同的行为:

由于传递指针在异常处理中通常是不合习惯的,因此,这里并不讨论传递指针。

如果捕捉端是以catch by value捕捉异常:

void catch_test()
{
	try {
		throw_test();
	} catch(exception e) {
		// exception handler
	}
}

就会调用拷贝构造函数对e进行构造。因此,如果以值捕捉异常,通常会有两次拷贝构造函数的调用。

如果捕捉端是以catch by reference捕捉对象:

void catch_test()
{
	try {
		throw_test();
	} catch(exception& e) {
		// exception handler
	}
}

就会将e绑定到那个堆上的临时对象。因此,引用传递只有一次拷贝构造函数的调用,而且,将一个基类的引用绑定到一个派生类的对象,该引用还可以使用多态的行为。

而如果以值来捕捉异常,抛出的是派生类的对象,捕捉时用的是基类的对象,此时,会发生对象切割,没有多态行为。(跟对象的参数传递类似)

这也验证了第1部分中最好以引用捕捉异常。


3 异常说明

异常说明是对一个函数抛出的异常的类型进行说明的一种方式,它能够表明该函数抛出哪些类型,在处理端就能够预料需要处理哪些异常。

异常说明通常有三种形式:

void func() throw (); //不抛出异常

void func() throw (type1, type2); //抛出类型为type1和type2的异常

void func(); //抛出任何类型的异常,网上有些文章说抛出任何类型的异常可以是void func() throw (...);但是在g++编译不通过


因此,在编写函数的时候,如果确定某个函数会抛出某个类型的异常,就可以加上异常说明来说明该函数抛出的异常类型。

当然,如果一切正常工作当然很好,函数抛出某种类型的异常,调用端就处理什么样的异常,但是,如果函数抛出的异常与函数的异常说明不符会发生什么情况呢?

如果函数抛出的类型与异常说明不符,就会调用一个特殊的函数unexpected(),它的默认行为是调用terminate(),而terminate()的默认行为是abort(),也就是如果一个函数违反了自己的异常说明,抛出与异常说明不符的异常,程序就会直接结束。

但是,异常说明通常是一个提示作用,告诉程序员该函数会抛出哪些异常,程序员在调用端需要处理哪些异常,如果函数常常抛出与异常说明不符的异常,那么,异常说明就没有用了,因为它完全起不到提示程序员的作用。

如下面的例子:

void catch_test(int x) throw (int)
{
	if(x == 0) {
		throw 3.0f;
	}
        throw 0;
}

函数的异常说明是int,但是函数体中却抛出了float类型的异常,大家知道,异常通常是不能进行类型转换的,因此,如果程序调用了catch_test(),就会导致程序异常结束。

解决的办法有三种:

(1)修改异常说明。

(2)调用set_unexpected()修改函数抛出异常与异常说明不一致时的回调函数,将不一致的异常修改为统一的异常。

class unexpected_exception {
};

void convert_unexpected()
{
	cout << "throw unexpected exception" << endl;
	throw unexpected_exception();
}

void catch_test(int x) throw (int, unexpected_exception)
{
	if(x == 0) {
		throw 3.0f;
	}
}

int main()
{
	set_unexpected(convert_unexpected);
	
	try {
		catch_test(0);
	} catch(int x) {
		cout << x << endl;
	} catch(unexpected_exception &x) {
		cout << "catch unexpected exception" << endl;
	}
}

如上面代码所示,在main函数的开始,调用set_unexpected()将回调函数设置为convert_unexpected(),也就是说,当函数抛出的异常与异常说明类型不一致时,会调用convert_unexpected(),而在convert_unexpected()函数中,抛出一个unexpected_exception异常,因此,任何与异常说明不一致的异常都会重新抛出unexpected_exception类型的异常。

在主函数中对catch_test()进行异常检查,当catch_test()的参数是0时会抛出float类型的异常,与异常说明的int不一致,因此,当调用catch_test(0)时,会调用convert_unexpected()函数,它会重新抛出unexpected_exception异常,在catch时可以对它进行处理。

这里要注意的是,在函数的异常声明中同样要加上unexpected_exception类型,因此,这里只是将unexpected()进行了一个替换,做了一个转换而已。

(3)在替换unexpected函数的函数中重新抛出该异常,该异常会被作为标准异常bad_exception重新抛出。

class unexpected_exception {
};

void convert_unexpected()
{
	cout << "throw unexpected exception" << endl;
	throw;
}

void catch_test(int x) throw (int, bad_exception)
{
	if(x == 0) {
		throw 3.0f;
	}
}

int main()
{
	set_unexpected(convert_unexpected);
	
	try {
		catch_test(0);
	} catch(int x) {
		cout << x << endl;
	} catch(bad_exception &x) {
		cout << "catch unexpected exception" << endl;
	}
}

如上面代码所示,在回调函数convert_unexpected()中使用throw;重新抛出该异常,它会被转换为bad_exception标准异常重新抛出。


4 关于异常需要注意的问题:

(1)在大型项目中,函数的调用层次比较深,程序员根本无法预料某个函数会抛出哪些异常,因为,它所调用的函数抛出的异常也会转嫁到它自己。在这种情况下,调用unexpected()的几率大大增加,如果一个函数认为自己不抛出异常,但是它调用的函数会抛出异常,那么,就会有3中的不一致问题。

(2)将异常说明与模板结合不是一个好主意,因为,模板的行为多变,更加不可预料。

(3)使用异常还需要考虑异常的成本,使用try语句进行异常的测试,当离开try时某些对象要进行销毁,因此,程序必须记住try语句开始和结束的位置,以便进行合理的处理。使用throw抛出一个异常会在堆上建立一个临时的异常对象,编译器负责对它进行销毁。这都是异常处理需要考虑的成本。所以,当确定程序并不抛出异常时,坚决不用异常处理,只在万不得已的情况下,才使用异常处理。


参考资料:

More Effective C++ 第三章:异常

你可能感兴趣的:(C++,异常处理,c++)