异常

        异常机制提供程序中错误检测与错误处理部分之间的通信。通过异常我们能够将问题的检测和解决分离,这样程序的问题检测部分可以不必了解如何处理问题。C++的异常处理中包括:

  • throw表达式(throw expression),错误检测部分使用这种表达式来说明遇到了不可处理的错误,throw引发(raise)了异常条件
  • try块(try block),错误处理部分使用它来处理异常。try语句块以try关键字开始,并以一个或多个catch子句(catch clause)结束。在try块中执行的代码throw出的异常,通常会被其中一个catch子句处理,catch子句也称为处理代码handler)。如果选择了一个catch子句来处理异常,则执行相关的块语句。一旦catch子句执行结束,程序流立即继续执行紧随着后一个catch子句的语句。如果不存在处理该异常的catch子句,程序将跳到名为terminate的标准库函数,该函数在exception头文件中定义(通常它的执行会导致程序非正常退出,在没有try块处理异常的程序中,系统以同样的方式对异常进行处理)。
try{
    program-statements
}catch(exception-specifier){
    handler-statements
}
catch(exception-specifier){
    handler-statements
}//...
//try语句内的program-statement形成程序的正常逻辑。
//异常说明符(exception specifier),声明单个类型或单个对象。
  • 有标准库定义的一组异常类exception class),用来在throw和相应的catch之间传递有关的错误信息。


        一些预处理变量
  • NDEBUG
  • __FILE__ 文件名
  • __LINE__ 当前行号
  • __TIME__ 文件被编译的时间
  • __DATE__ 文件被编译的日期




        assert(断言)预处理 (preprocessor macro)是在cassert头文件中定义的。断言可以有两种形式:
  • assert Expression1
  • assert Expression1:Expression2
        其中Expression1应该总是一个布尔值,Expression2是断言失败时输出的失败消息的字符串。
        如果Expression1为假,则抛出一个 AssertionError,这是一个 错误,而不是一个 异常,也就是说是一个不可控制异常(unchecked Exception)。AssertionError由于是错误,所以可以不捕获,但不推荐这样做,因为那样会使你的系统进入不稳定状态。
        assert是宏,不是函数。

        一个常见的调试技术是使用NDEBUG预处理变量以及assert
        assert(expr)
        只要NDEBUG未定义,assert就求解条件表达式expr。如果结果为false,assert输出信息并终止程序的执行;如果该表达式有一个非零值,则assert不做任何操作。

        前置条件断言:代码执行之前必须具备的特性
        后置条件断言:代码执行之后必须具备的特性
        前后不变断言:代码执行前后不能变化的特性





抛出类类型的异常


        异常是通过抛出(throw)对象而引发(raise)的,该对象的类型决定应该激活哪个处理代码,被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那个。异常以类似于将实参传递给函数的方式抛出和捕获,异常可以是可传给非引用形参的任意类型的对象,这意味着必须能够复制该类型的对象。
        因为在处理异常的时候会释放局部存储,所以被抛出的对象不能再局部存储,而是用throw表达式初始化一个称为异常对象(exception object)的特殊对象。异常对象由编译器管理,而且保证驻留在可能被激活的任意catch都可以访问的空间。这个对象右throw创建,并被初始化为被抛出的表达式的副本,异常对象将传给对应的catch,并且在完全处理了异常之后撤销。

        在实践中,许多应用程序所抛出的表达式,其类型都来自某个 继承层次,标准异常定义在一个继承层次中。当抛出一个表达式时,被抛出对象的静态编译时类型将决定异常对象的类型。通常,使用静态类型抛出对象不成问题。

        用抛出表达式抛出静态类型时,在抛出中对指针解引用,其结果是一个对象,其类型与指针的类型匹配。如果指针指向继承层次中的一种类型,指针所指对象的类型有可能与指针的类型不同。异常对象的类型与指针的静态类型相匹配。如果指针是一个指向派生类对象的基类类型指针,则那个对象将被分割,只抛出基类部分。
        如果抛出指针本身,可能引发比分割对象更严重的问题。具体而言,抛出指向局部对象的指针总是错误的,其理由与从函数返回局部对象的指针是错误的一样。




栈展开


        抛出异常的时候,将暂停当前函数的执行,开始查找匹配的catch子句。首先检查throw本身是否在try块内部,如果是,检查与该try相关的catch子句,看是否其中之一与被抛出对象相匹配。如果找到匹配的catch,就处理异常;如果找不到,就退出当前函数(释放当前函数的内存并撤销局部对象),并继续在调用这个函数的函数中查找。这个过程称之为栈展开(stack unwinding)。

        栈展开期间,提早退出包含throw的函数和调用链中可能的其它函数。因异常而退出函数时,编译器保证适当地撤销局部对象。通常,如果局部对象是类类型的,编译器自动调用该对象的析构函数,但不撤销内置类型的对象。因此,可对异常使用类管理资源分配的编程技术。

        在为某个异常进行栈展开的时候,析构函数如果又抛出自己的未经处理的另一个异常,将会导致调用标准库terminate函数。一般而言,terminate函数将调用absort函数,强制从整个程序非正常退出。 析构函数应该从不抛出异常,标准库类型都保证它们的析构函数不会引发异常。

        与析构函数不同,构造函数内部经常会抛出异常,此时该对象可能只是部分被构造。即使如此,也要保证适当地撤销已构造的成员。类似地,在初始化数组或其它容器类型的元素发生异常时,同样要保证适当地撤销已构造的元素。

        不能不处理异常,如果找不到匹配的catch,程序就调用库函数terminate。




捕获异常


        catch子句中的异常说明符(exception specifier)看起来像只包含一个形参的形参表,即异常说明符是一个类型名,其后跟一个(可选的)形参名。异常说明符可以省略形参名,如果处理代码需要已发生异常的类型之外的信息,则catch可使用这个名字访问异常对象。类型必须是完全类型,即内置类型或者已经定义的程序员自 定义类型,不能是类型的前向声明。

        在查找匹配的catch期间,将选中第一个找到的可以处理该异常的catch,异常与catch异常说明符匹配的规则比实参和形参类型的匹配规则更严格,除下面几种可能的区别之外,异常的类型和catch说明符必须完全匹配(不允许标准算术转换,也不允许为类类型定义的转换):
允许从非const到const的转换
允许从派生类类型到基类类型的转换
将数组转换为指向数组类型的指针,将函数转换为指向函数类型的适当指针

        进入catch的时候,如果说明符不是引用,则异常对象复制到catch形参中,对形参所做的任何改变都只作用于副本,不会作用于异常对象本身;如果说明符是引用,则catch形参只是异常对象的另一名字,对catch形参所做的改变作用于异常对象。

        基类的异常说明符可以用于捕获派生类类型的异常对象,且其静态类型决定catch子句可以执行的动作。如果catch形参是引用类型,catch对象就直接访问异常对象;如果不是引用,则catch对象是异常对象的副本,基类类型的catch对象分割为派生类类型的异常对象的基类子对象。

        catch子句的次序应该反映类型层次,使用来自继承层次的异常的程序应该将它们的catch子句排序,以便派生类型的处理代码出现在其基类类型的catch之前。




重新抛出


        catch可以通过重新抛出(rethrow)将异常传递给函数调用链中更上层的函数,重新抛出是后面不跟类型或表达式的一个throw,空throw语句将重新抛出 异常对象。被抛出的异常是原来的异常对象,而不是catch形参(当异常说明符是引用的时候,异常对象可能会被改变)。




捕获所有异常的处理代码


        捕获所有异常的catch子句形式为 catch(...),捕获所有异常的catch子句与任意类型的异常都匹配。catch(...)经常与重新抛出表达式结合使用,完成可做的所有局部工作,然后重新抛出异常。有时候,当catch(...)与其它catch子句结合使用,应该是最后一个,否则任何跟在它后面的catch子句都不能被匹配。




函数测试块与构造函数


        为了处理来自构造函数初始化式的异常,必须将构造函数编写为函数测试块(function try block)。可以使用函数测试块将一组catch子句与函数联成一个整体,如
template <class T>
Handle<T>::Handle(T *p)
try: ptr(p), use(new size_t(1)){
    //...
}catch(const std::bad_alloc &e){
    //...
}




用类管理资源分配


        程序的异常安全(exception safe),即即使发生异常,程序也能正确操作。在这种情况下,“安全”来自于保证“如果发生异常,被分配的任何资源都适当地释放”。通过定义一个类来封装资源的分配和释放,可保证正确释放资源,这一技术常称为“资源分配即初始化”,简称RAII( Resource Acquisition Is Initialization)。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值