转自:http://www.frontfree.net/view/article_755.html
原创:monkeyfu
处理在程序的运行时刻发生的错误,对于任何一个程序设计者来讲都是不陌生的。对于错误的处理,我们有很多方法,本篇着重介绍的是C++中的错误异常处理。
在介绍C++中的错误异常处理之前,我们先来看一下常用的错误处理方式。
1.返回值 |
可以说这是最常用的错误处理方式之一,但其存在着一个致命的问题。就是返回值的检查与否是由调用者主动控制的。如果调用者不检查返回值,那也没有任何
机制能够强迫他这么做。再一个,考虑在C++中参数表相同而返回值不同的重载情况。在这种情况下,如果调用者不检查返回值的话,编译器根本不清楚应该调用哪个函数。 |
2.全局状态标示符 |
这种办法同返回值一样,也是需要调用者主动检查的。并且由于其是全局的,因此在多线程程序中,还必须保证它的线程安全性,必须要让检查者知道这是谁的返回值。 |
3.setjmp()/longjmp() |
你完全可以将longjmp()当成远程的goto语句进行调用(goto语句只能左右于本地函数里)。但这个函数却存在着很大甚至是致命的危险。暂且放下该函数会破坏结构化程序设计风格不说。其一,longjmp()只能处理int型的异常。其二,也就是最致命的一点就是,longjmp()不会调用析构函数,而C++的
异常处理机制却会完成这个事情。因此,在C++中,千万不要使用setjmp()、longjmp()函数。 |
4.断言 |
对于断言(Assert),其仅仅是在Debug版本中起作用,在Release中其是不存在的。另外断言与我们通常所说的错误处理方式不同,他是用来处理我们可能会发生这个错误,并能够避免的这种情况。 |
在介绍过上面那些存在问题的错误处理方式后,现在让我们来看看C++中的异常机制是如何处理错误的。首先说,C++的异常处理不会像上面提到的那些方法一样,必须是调用着主动检查。因为在C++中,一旦抛出(throw)一个异常,而程序不捕获(catch)的话,那么最终的结果就是abort()函数被调用,使得程序被终止。
下面我们来看一下C++异常处理(以下称EH)的基本语法和语意。
其引入了3个关键字,分别是:
catch, throw, try
throw
异常由throw抛出,其格式为
函数在定义时通过异常规格申明定义其会抛出什么类型的异常,其格式为:
type-ID-list是一个可选项,其中包括了一个或多个类型的名字,它们之间以逗号分隔。
例如:
void func()
throw(int, some_class_type) |
则表明会抛出int和some_class_type类型异常。
对于一个空的异常规格申明,表示不抛出任何异常。
如:
而如果函数没有异常规格申明,则表示会抛出任何类型的异常。
不过这里存在一种情况,例如:
void func()
throw(int) //指明抛出int型异常
{
...
subfunc(); //但可能从这里抛出非int型异常
...
} |
try -- catch
try块中的异常处理函数对异常进行捕获。其可以包含一个或多个处理函数,其形式如下:
catch (exception-declaration)
compound-statement |
处理函数的异常申明指明了其要捕获什么类型的异常。
对于异常申明其可以是无名的,例如:catch(char
*),其表明会捕获一个char *类型异常,但由于是无名的,因此不能对其进行操作。另外异常申明也可以存在如下形式:catch(...),其表明会捕获任何类型的异常。
举例:
void
func() throw(int,
some_class_type)
{
int
i;
........
throw
i;
........
}
int main()
{
try
{
func();
}
catch(int
e)
{
//处理int型异常
}
catch(some_class_type)
{
//处理some_class_type型异常
}
.......
return
0;
}
|
从上面的例子可以看出,当函数抛出异常时,throw后面要带一个抛出的对象。但这并不是必须的,例如:
catch(int
e)
{ .......
throw;
} |
throw后面没有接任何对象,这表明throw会再次抛出已存在的异常对象,因此其必须位于catch块中。
下面介绍一些C++提供的标准异常
namespace
std
{
//exception派生
class logic_error;
//逻辑错误,在程序运行前可以检测出来
//logic_error派生
class domain_error; //违反了前置条件
class invalid_argument; //指出函数的一个无效参数
class length_error; //指出有一个超过类型size_t的最大可表现值长度的对象的企图
class out_of_range; //参数越界
class bad_cast; //在运行时类型识别中有一个无效的dynamic_cast表达式
class bad_typeid; //报告在表达试typeid(*p)中有一个空指针p
//exception派生
class runtime_error;
//运行时错误,仅在程序运行中检测到
//runtime_error派生
class range_error; //违反后置条件
class overflow_error; //报告一个算术溢出
class bad_alloc; //存储分配错误
}
|
在C++标准库头文件<exception>中申明了几个EH类型和函数,它们是:
namespace
std
{
//EH类型
class bad_exception;
class exception;
typedef void (*terminate_handler)();
typedef void (*unexpected_handler)();
// 函数
terminate_handler set_terminate(terminate_handler) throw();
unexpected_handler set_unexpected(unexpected_handler) throw();
void terminate();
void unexpected();
bool uncaught_exception();
}
|
exception |
是所有标准库抛出的异常的基类。 |
uncaught_exception() |
函数在异常被抛出却没有被捕获时返回true,其它情况返回false |
terminate() |
在异常处理陷入了不可恢复状态,如:重入时被调用。 |
unexpected() |
在函数抛出一个没有在“异常规格申明”中申明的异常时被调用。 |
运行库提供了缺省terminate_handler()和unexpected_handler()函数处理对应的情况。你可以通过set_terminate()和set_unexpected()函数替换库的默认版本。这两个函数,其可以获取不带输入输出参数的函数,并且该函数会返回原terminate或者unexpected函数的地址指针。以便在使用中调用或者以后的恢复。另外,在terminate
()中。其必须不返回或者抛出异常。
在介绍了EH的基本知识后让我们来看看EH是如何工作的。
一般来说当发生函数调用的时候,都会进行诸如,保存寄存器值,参数压栈,创建被调函数堆栈等保护现场的工作,而在函数返回的时候则会进行与此相反的恢复现场的工作。
这样,当一个异常发生时,程序会在异常点处停止,然后开始搜索异常处理函数,其过程同函数返回相同,延调用栈向上搜索,直到找到一个与异常对象类型像匹配的异常申明,并进行相应的异常处理函数,在异常处理结束后,程序跳到异常处理函数所在try快最接近的下面一条语句开始执行。如果没有找到合适的异常申明,则最终会调用std
:: unexpected(),并在其中调用std:terminate()直到abort(),程序被终止。
这也就意味着C++对于异常处理的模式始终是终止的。
例如:
#include
<iostream.h>
static void func(int n)
{
if (n)
throw 100;
}
extern int main()
{
try
{
func(1);
cout<<"程序不会执行到这里"<<endl;
}
catch(int)
{
cout<<"捕获一个int型异常"<<endl;
}
catch(...)
{
cout<<"捕获任意类型异常"<<endl;
}
cout<<"继续执行"<<endl;
return 0;
}
|
该程序在运行时会打印如下信息:
捕获一个int型异常
捕获任意类型异常
至于异常处理的另一种模式恢复模式。可以通过循环检测直到结果满意为止。但在实际中,往往产生异常的地方与异常处理函数距离可能会比较远,在这种情况下恢复模式就不那么可行了。
虽然,在异常处理延调用栈向上走的过程中回析构所有栈上的对象,但其并不会对堆中的对象进行处理,这样将会引起严重的资源泄露问题。
例如:
void
func()
{
testclass *p = new testclass();
...
test(p); //这里会抛出异常
...
delete p;
//在抛出异常后,这里不会被执行,因此会导致内存泄露问题。
}
|
为了解决这个问题,C++提供了std::auto_ptr模板。其原理就是,将指针用一个栈上的模版实例保护起来,当发生异常的时候,模版会被析构,在析构函数中指针也就被delete了。
例如:
void func()
{ std::auto_ptr<testclass> p(new
testclass()); ... test(p.get());
...
} |
另外,在构造函数中抛出异常并不会引发析构函数。这一点要十分注意。因为这也会产生资源泄露问题。
例如:
class
test
{
public:
test() { c = new char[10]; throw -1;}
~test() {delete c;}
private:
char *c;
};
void proc()
{
try{
test t;
}
catch(int)
{
.......
}
}
|
由于异常是在test的构造函数中产生的,因此其不会引发其析构函数的调用。于是就如程序所示,产生了内存泄露问题。对于这种问题,最好的解决办法还是使用auto_ptr。
对于析构函数,则不要在其中抛出异常。其原因在于析构函数会在其他异常抛出时被调用,这样就会引发异常的重入问题,进而导致terminate()被调用。如果在析构函数中真要抛出异常,如:析构函数调用的函数会抛出异常等,则必须在该析构函数内将其捕获。
前面说到要“找到一个与异常对象类型像匹配的异常申明”。事实上,这种匹配并不要求的十分准确。
考虑如下例子:
#include
<iostream.h>
class base
{
public:
virtual void what()
{
cout << "base" << endl;
}
};
class derived: public base
{
public:
void what()
{
cout << "derived" << endl;
}
};
void f()
{
throw derived();
}
main()
{
try
{
f();
}
catch(base b)
{
b.what();
}
try
{
f();
}
catch(base& b)
{
b.what();
}
}
|
其显示结果为:
base
derived
为什么会这样呢。因为如果异常抛出一个派生类对象,而恰好又其基类所捕获到。那么该对象会被做"切片"处理。也就是说相对于基类,派生元素会被割下。在例子中derived的vptr会被设为base的virtual
table。因此虚函数what就会呈现出这种行为。而当通过引用捕获时,得到的仅仅是其地址,对象不会被做切片处理。vptr因此也就不会发生变化,所以what仍然呈现出来derived的行为。
因此,这也就提醒我们将基类处理放在最后,在实际中更有意义。因为这样可以尽可能的在前面的处理中保存信息。