下面我们来看一下C++异常处理(以下称EH)的基本语法和语意。
其引入了3个关键字,分别是:
catch, throw, try
其引入了3个关键字,分别是:
catch, throw, try
throw
异常由throw抛出,其格式为
异常由throw抛出,其格式为
|
throw [expression]
|
函数在定义时通过异常规格申明定义其会抛出什么类型的异常,其格式为:
|
throw([type-ID-list])
|
type-ID-list是一个可选项,其中包括了一个或多个类型的名字,它们之间以逗号分隔。
例如:
|
void func() throw(int, some_class_type)
|
则表明会抛出int和some_class_type类型异常。
对于一个空的异常规格申明,表示不抛出任何异常。
如:
对于一个空的异常规格申明,表示不抛出任何异常。
如:
|
void func() throw(...)
|
而如果函数没有异常规格申明,则表示会抛出任何类型的异常。
不过这里存在一种情况,例如:
不过这里存在一种情况,例如:
|
void func() throw(int) //指明抛出int型异常
{ ... subfunc(); //但可能从这里抛出非int型异常 ... } |
try -- catch
try块中的异常处理函数对异常进行捕获。其可以包含一个或多个处理函数,其形式如下:
try块中的异常处理函数对异常进行捕获。其可以包含一个或多个处理函数,其形式如下:
|
catch (exception-declaration) compound-statement
|
处理函数的异常申明指明了其要捕获什么类型的异常。
对于异常申明其可以是无名的,例如:catch(char *),其表明会捕获一个char *类型异常,但由于是无名的,因此不能对其进行操作。另外异常申明也可以存在如下形式:catch(...),其表明会捕获任何类型的异常。
举例:
对于异常申明其可以是无名的,例如: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型异常
捕获任意类型异常
至于异常处理的另一种模式恢复模式。可以通过循环检测直到结果满意为止。但在实际中,往往产生异常的地方与异常处理函数距离可能会比较远,在这种情况下恢复模式就不那么可行了。
捕获一个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的行为。
base
derived
为什么会这样呢。因为如果异常抛出一个派生类对象,而恰好又其基类所捕获到。那么该对象会被做"切片"处理。也就是说相对于基类,派生元素会被割下。在例子中derived的vptr会被设为base的virtual table。因此虚函数what就会呈现出这种行为。而当通过引用捕获时,得到的仅仅是其地址,对象不会被做切片处理。vptr因此也就不会发生变化,所以what仍然呈现出来derived的行为。
因此,这也就提醒我们将基类处理放在最后,在实际中更有意义。因为这样可以尽可能的在前面的处理中保存信息。
本文详细解析了C++中的异常处理机制,包括throw、try和catch关键字的使用方法,异常规格声明的作用,以及如何捕获和处理各种类型的异常。同时介绍了C++标准库中提供的异常类型,并给出了具体的代码示例。
977

被折叠的 条评论
为什么被折叠?



