内容整理自大黑书《C++程序设计语言》
目的
- 捕捉处理、以及重新抛出
- 资源管理
- 当异常是能够预期到且被捕捉到,可以用来作为一种控制结构或者返回方式。
设计原则
-
异常结组:抛出通过异常基类派生的子异常。在捕捉的时候捕捉基类异常,这样利于可扩展的开发。需要注意的是
catch
到的异常对象需要通过指针传递或者引用传递。否则可能出现异常对象的截断。例如: -
多个异常:通过多继承设计的子类,继承了多个父类。这种异常的实例能够组合多个异常,此时这样的一个异常对象能够被多个处理异常的函数捕捉。
-
重新抛出:当捕捉了一个异常之后,不一定需要完全地将异常处理。可以在局部完成能够做的事,然后将异常抛出。重新抛出的表示方法如下(不带操作对象的
throw
):void h(){ try{ //可能抛出Matherr的代码 } catch(Matherr){ if(can_handle_it_completely){ //处理Matherr return; } else{ //完成这里能做的事 throw;//重新抛出异常 } } }
-
资源申请即初始化:如果对象初始化初始化操作发生异常并抛出,此时已经申请的资源(例如指针)不会释放。因为对象所需要的资源在其析构函数中释放,对象构造函数发生异常未完成。所以析构函数不会被调用。一种解决方法是将资源通过容器对象申请,当异常抛出,发生栈回退,此时容器对象离开作用域,其析构函数就会被调用,资源就可以得到释放。
标准库也提供了模板类auto_ptr
,支持资源申请即初始化,这样可以按照原来的方式使用指针初始化资源而不使用容器类等对象,从而不用改变使用方式。auto_ptr
离开作用域时,被它所指向的对象也能隐式地自动删除。但是auto_ptr
的设计只保证了自动指针的异常安全性,但是其破坏性复制语义会导致如下行为的发生:auto_ptr
具有称为破坏性复制语义(distructive copy semantics)的所有权语义(owner semantics)。在讲一个auto_ptr
复制给另一个之后,原来的auto_ptr
不再指向任何东西。因为复制auto_ptr
将导致对它本身的修改,所以const auto_ptr
就不能被复制。auto_ptr
支持类型转换auto_ptr<A>
能够转换为auto_ptr<B>
,如果A
能够转换为B
。- 对于
auto_ptr
引用的指针,不要用多个auto_ptr
来引用,这样的结果是程序未定义的,可能导致指针被删除多次。最好将在auto_ptr
之间转移引用和所有权。 auto_ptr
的破坏性复制语义使其不满足标准容器或标准算法。例如sort()
对元素的基本要求。
资源申请即初始化适用于不满足于简单终止程序或简单相应的程序,例如==库设计==,此时采用“资源申请即初始化”策略,并用异常发出运行错误信号。
-
异常和new:如果对象的的构造函数抛出了异常,但是没有使用放置语法,那么通过operator new()分配的存储能够得到释放。否则的话如果进行了非标准的分配,具有Z::operator new(),那么就需要调用相应的Z::operator delete()。
放置语法:在特定的内存位置进行了初始化操作???
-
资源耗尽:
结束当前计算并返回某个调用程序。调用程序应该准备好应对某个资源申请失败的情况。
设置处理器,先处理看能不能找到资源。如果找不到资源,才抛出错误。如果找不到处理器,也抛出错误。将处理方式交给调用程序来决定。
每个C++实现都要注意保留足够的存储,即使在存储耗尽的情况也能够抛出异常。异常是一个对象。
-
构造函数里的异常:
三种从构造函数中报告错误的非异常解决方案:
- 返回一个处于错误状态的对象,让用户来检查对象的状态。
- 设置一个非局部变量,例如errno,让用户检查变量。
- 构造函数中不进行初始化,在用户第一次使用对象前使用Object.init()完成初始化。或者将对象标记为未初始化状态,让使用这个对象的第一个成员函数进行实际的初始化。
使用错误处理机制
- 异常和成员的初始化,可以在对象的成员的初始式中捕捉异常
- 异常和复制,复制构造函数需要释放申请到的所有资源
-
析构函数中的异常:
从析构函数被调用的角度来说,有两种可能,
- 某个作用域正常退出,调用delete的结果。
- 异常处理中被调用,在堆栈回退的过程中,异常处理机制推出一个作用域时,包含有具有析构函数的对象。
绝对不能让析构函数里抛出异常,否则就看作是异常处理的一次失败,并调用
std::terminate()
。如果析构函数要调用一个可能抛出异常的函数,则要自己进行处理。
如果某个一场已被抛出但是并未被处理,那么标准库函数uncaught_exception()
就会返回true。程序员可以在析构函数中判断对象是被正常销毁还是作为堆栈回退中的一部分,进而执行不同的操作。
异常的其他用法
- 异常的本质是堆栈的回退以及“异常”信息的传递。
- 用作一种返回方式,特别是在一些高度递归的检索函数中,例如:
void fnd(Tree* p, const string& s){ if(s == p->str) throw p; //如果找到s if(p->left) fnd(p->left, s); if(p->right) fnd(p->right, s); } Tree* find(Tree* p, const string& s){ try{ fnd(p, s); } catch(Tree* q){ //q->str == s return q; } }
异常的描述
- 在函数声明中
void f(int a) throw (x2, x3);
表示只可能抛出两个异常x2和x3,以及从它们派生的异常。
如果抛出了其他异常,就会产生对std::unexpected()
的调用,upexpected()
的默认意义是std::terminate()
,它通常转而调用abort()
。unexpected()
不会返回。带有异常描述和不带有异常描述的函数能够相互转换,带有异常描述的函数更简洁,另外这样属于将异常的描述放在界面(函数声明),调用者不需要查看函数定义(另外函数定义一般不可用)。
不带有异常描述的函数声明假定它可能抛出任何异常。
int g() throw();
异常类型集合为空表示函数不抛出异常。 - 函数声明中如果包含了异常类型描述,那么函数的定义中也要有一致的异常类型描述。
- 在覆盖一个虚函数的时候,这个函数所带的异常描述必须至少是与被覆盖虚函数一样受限制的。更受限是指指出更少可能抛出的异常的类型如:
class B{ public: virtual void f(); //可以抛出任何异常 virtual void g() throw(X,Y); virtual void h() throw(X); }; class D: public B{ public: void f() throw(X); //ok void g() throw(X); //可以:D::g() 比 B::g()更受限 void h() throw(X, Y); //错误:D::h()不如B::h()那么受限 }
- 异常描述亦可以用于函数指针。同上,被引用函数也应该至少与函数指针的异常描述一样受限。函数指针能够抛出的异常应该要更多一些。由于异常描述不是函数类型的一部分,所以
typedef
不能带有异常描述,如:typedef void (*PF)() throw(X); //错误
- 对于未预期的异常,可以将要设计的系统中可能的异常都从一个基类派生,然后,函数的异常描述中包含该基类。这样函数里的积累基类都不会出发unexpected()。另外,标准库抛出的所有异常都是由exception派生的。
- (这里有点疑惑)对于未预期的异常,如果调用
unexpected()
直接终止程序可能太过粗糙或者严厉,如果又不想重写函数(即加入其他可能的异常。),此时需要将unexpected()
的行为修改为其他可以接受的行为。这个称为异常的映射。
(1) 首先将标准库异常std::bad_exception
加入异常描述。此时unexpected()
将直接抛出一个bad_exception
,即捕获未预期的异常,然后抛出bad_exception
。这种方法会导致失去异常相关的信息。
(2) 此时可以重新定义unexpected()的意义。与_new_handler
与set_new_handle()
对应类似,_unexcepted_handler
与<exception>
中的set_unexcepted()
对应。
先采用资源申请即初始化技术为unexpected()
定义一个类:
定义一个函数,使它具有所希望的class STC{ unexpected_handler old; public: STC(unexpected_handle f) {old = set_unexpected(f);} ~STC() { set_unexpected(old);} }
unexpected()
的意义。在用作unexpected()
函数时,throwY()
将所有未预期的异常都映射为Yunexpected
。
此时一个函数class Yunexpected : public Yerr {}; void throwY() throw (Yunexpected) {throw Yunexpected();}
g()
,可以是这样的:
总结就是将未预期异常映射到预期异常,就是如果要达成如上效果,原本则需要在函数void networked_g() throw (Yerr) { STC xx(&throwY); //现在unexpected()抛出Yunexpected g(); }
g()
中加入遇到新异常时应该抛出的类型。再从内部或外部进行捕获处理。而通过保存和设置_unexpected_handler
,可以在几个子系统都能控制对未预期异常的处理工作又不会相互影响。
此时只是改变了遇到未预期异常时调用unexpected()终止程序的行为,但是还是不知道未预期异常的信息。此时需要找回异常的类型。
未捕捉的异常
- 如果抛出的异常未被捕获,就会调用函数
std::terminate()
。当异常处理机制发现堆栈损坏,或在某个异常导致的堆栈回退中,被调用的析构函数企图通过抛出异常而退出时,就会调用std::terminate()
。 _unexpected_handler
与set_unexpected()
对应,_uncaught_handler
与<exception>中的std::set_terminate()
。- 默认情况下,
terminate()
将调用abort()
。 _uncaught_handler
默认不会返回其调用者。- 在程序因为未捕捉的异常而终止时,是否调用有关析构函数由具体实现决定。不调用析构函数有时利于程序从排错系统中恢复;但是有些系统中,其系统结构决定了在检索异常处理器的过程中,不调用析构函数是不可能的(再研究研究)。
标准库异常
- 标准库异常是一个层次结构
- 所有标准库异常都派生自exception,包括了逻辑错误(程序执行之前,或通过检测函数和构造函数的参数捕捉到的错误)和运行时错误(其他所有的错误)。
- 不应该通过捕捉exception去捕捉所有的异常。