C++编程思想学习.下卷——异常处理

异常处理

增强错误恢复能力是提高代码健壮性的最有力的途径之一。

将try块嵌套在for、while、do或者 if快中,并且触发异常来试图解决问题,然后重新测试try块中的代码。

1.5 清理

(1).资源管理

异常处理的魅力之一在于程序能够从正常的处理流程中跳转到恰当的异常处理器中。如果异常抛出时,程序不能做恰当的清理工作,那么异常本身并没有什么用处。编写代码终于到异常时,就应该要注意一旦异常发生,那么资源是否被正确清理了。大多数情况不用担心,但是,如果在一个对象的构造函数中抛出异常时就要特别注意,因为对象的构造函数中一旦抛出异常,那么这个对象的析构函数就不会被调用,从而,在构造函数中分配的资源就不会被释放,这样就发生了内从泄露,同样,经常会伴随着“悬空指针”(naked pointer)。为了防止资源泄露,可以使用以下两种方式来资源分配:

(a).在构造函数中来捕获异常,用于释放资源。

(b).在对象的构造函数中分配资源,并且在对象的析构函数中释放资源。

(2)使所有事物都成为对象

资源获得初始化技术:使资源分配成为局部对象生命周期的一部分(如通过模版(在模板中包含了对象的动态创建,并在模板中设置成员指针指向创建的对象),使得对象资源的分配成为局部生命周期),那么如果某次资源分配失败了,那么在栈反解析的时候、其他已经获得所需资源的对象能够被恰当的清理。

(3) auto_ptr

在一个典型的C++程序中动态分配内从是频繁使用的资源,所以,C++标准中提供了一个RAII封装类,用于指向分配的堆内存的指针,这就使得程序能够自动释放这些内存。Auto_ptr类模板定义于头文件<memory>中。使用auto_ptr不需要显示地删除原始指针,因为在栈反解的时候auto_ptr的对象会自动的删除原始指针。由于,通过值引用的类对象总会被析构,所以当对象被析构时,这个对象的auto_ptr成员总是能释放它所封转的原始指针,所以,auto_ptr类模版可以很容易用于指针数据成员。

(4)标准异常

       所有的标准异常类都是从exception类派生而来,它主要派生出类:logic_error和runtime_error类,logic_error用于描述程序中出现的逻辑错误,如:传递无效的参数。运行时错误是指那些无法预料的事件所造成的错误,如:硬件故障、内存耗尽。Logic_error和runtime_erroe提供了一个std::string的构造函数,这样就可以将消息保存在这两种类型的异常对象中,通过exception::what()函数,读者可以从对象中得到它所保存的消息。而std::exception却没有提供这样的构造函数,所以,用户最好从runtime_error和logic_error来派生自己的异常,而不要直接从std::exception类派生。

       输入、输出流ios::failure也是从exception派生而来,但是,它没有子类。我们一般使用下面所列的异常类,或者把他们作为基类来派生自己的更加具体的异常类:

      

从logic_error派生而来的异常类

Domain_error :报告违反了前置条件

Invalid_argument: 表明抛出这个异常的函数接收到了一个无效的参数

Out_of_range :报告一个参数越界错误

Bad_cast: 抛出这个异常的原因是运行时类型识别(runtime type identification)中发现程序执行了一个无效的动态类型转化(dynamic_cast)表达式.

Bad_typeid: 当表达式typeid(*p)中的参数p是一个空指针是抛出这个异常(这个也是运行时类型识别的特性)

 

从runtime_error派生的异常类

Range_error:报违反了后置条件

Overflow_error:算数溢出错误

Bad_alloc:报告一个失败的存储分配

(5)、异常规格说明

异常规格说明,它是函数声明的修饰符,写在参数列表的后面。它再次使用关键字throw,函数可能抛出的所有异常的类型应该被写在throw之后的括号里面。如下所示:

Void f() throw(toobig,toosmall,divzero);这个函数可能会抛出的三种异常。当函数抛出异常时最好使用异常规格说明。如果,函数所抛出的异常没有列在异常规格说明的异常集中,那么默认的函数unexpected()会被调用,默认的unexpected()会调用。会调用terminate()函数。

Set_unexpected()函数:同terminate()一样可以使用这个set_unexpected()函数来设置用户自己的意外的异常。编程人员必须包含<expection>头文件。

(6) 异常规格说明和继承

       由于异常规格说明在逻辑上也是函数声明的一部分,所以在继承层次结构上也必须保持一致(函数的返回值可以发生协变,但参数不可以,参数必须一致)。

(7)什么时候不用异常规格说明

       当无法知道会发生什么异常时,不要用异常规格说明,例如:模版函数或者模版类。

1.9在编程中的异常使用指南

(1)什么时候避免使用异常

       只有当函数不符合它的规格说明的时候才抛出异常。

1、  不要再异步事件中使用异常

由于异常和它的处理器必须处于相同的函数调用栈上,而异步事件必须由完全独立的代

码来处理,这些代码不是正常流程中的一部分(典型的例子是中断服务程序和事件循环),所以,无法使用C++中的异常来处理异步事件,所以,不要在中断处理程序中抛出异常。所以,处理这种异常的典型方法是,中断处理程序设置一个标记,程序的主干代码同步的检查这个标记。

2、  不要在处理简单错误是使用异常

如果能够得到足够的信息来处理错误,那么就不要使用异常。程序员应该在当前语境中处理这个错误,而不是将一个异常抛出到更大的上一层的语境中。

此外,C++在遇到机器层时间如除零错误时,不会抛出异常。读者可以认为其他一些机制,如操作系统或者硬件会处理这种事件。这样异常处理可以专注的处理程序级的异常。

3、  不要讲异常用于程序的流程控制

4、  不要强迫自己使用异常

某些程序是相当简单的,显示一个消息然后退出程序就可以了,最好把清理工作交给操作系统,而不必费尽地捕获所有异常并释放所有资源。简单地说如果读者不需要异常,就不要强迫自己使用它们。

5、  新异常,老代码

在老代码中,添加了出抛出新的异常的代码。那么可以在使用新代码的覆盖范围最大的代码放入try块中(可能是整个main函数)。然后追加一个catch(…)。或者更好的办法是将发生异常的代码隔离在try块中。

(2) 什么时候使用异常

在下列情况下使用异常:

修正错误并重新调试产生异常的函数

在重新调试中的函数外面补偿一些行为以便使程序得以继续执行。

在当前语境中做尽可能多的事,并把同样类型的异常重新抛出到更高的层次的语境中。

在当前语境中做尽可能多的事,并把一个不同类型的异常抛出到更高层次的语境中去。

终止程序

将使用不同错误处理模式的函数(尤其是C库函数)封装起来,以便使用异常来代替原有的错误处理模式。

简化。如果建立的错误处理模式使事情变得复杂并且难以使用,那么异常可以使错误处理更加简单有效得多。

使建立的库和程序更加安全。使用异常既是一种短期投资(方便调试)优势一种长期投资(系统健壮)。

1、  什么时候使用异常规格

异常规格说明就想函数原型:具有提醒使用者的作用。因为总会出现无法预料的异常,

所以,如果要使用异常规格说明的话,最好编写自己的unexpected()函数,在这个函数中将异常消息记录到日志中,然后抛出异常或者终止。

2、  从标准异常开始

如果,系统的异常类能满足程序的要求,那么就是用系统的异常类。如果系统的异常类

不能满足程序的要求尽量从现有的标准异常类中继承出一个。尽量让用户可以使用what()函数接口。

3、  嵌套用户自己的异常

如果为用户自己的特定类创建异常,最好在这个特定的类中或者包含这个特定类的命名

空间中嵌套异常类,这就为读者提供了一个明确的信息——这个异常类只在用户自己定义的特定类中使用,也防止污染全局名字空间。即使这个异常类是从标准异常类中继承而来。

4、  使用异常层次结构

异常层次结构为用户的类或库可能遇到的不同类型的重要错误提供一个有价值的分类

方法。

5、多重继承(MI)

       唯一必须要用到多重继承的情况是:当需要将一个对象的指针向上类型转换成两个不同的基类类型时——也就是说,读者同时需要这两个基类的多态行为。多态继承的异常类的任何一个基类的异常处理都能处理这个异常。

5、  通过引用而不是通过值来捕获异常

好处:a)当异常对象被传递到异常处理器中的时候,避免进行不必要的对象拷贝。

a)        当派生类对象被当做基类对象捕获时,避免对象切割。

b)       如果,采用捕获指针类型的异常,则会在代码中引进紧耦合——抛出异常的代码和捕获异常的代码必须协商好,内从由谁释放、有谁开辟,同时由于在堆内存耗尽的时候也可能会发生异常,所以,这也造成一个问题。如果程序抛出异常对象,一场系统负责处理所有与存储有关的问题。

6、  在构造函数中抛出异常

有两种方法来报告在构造对象期间发生的异常:

a)        设置非局部的标记,并且希望用户检查它

b)       返回一个未完成的创建对象,并且希望用户检查它。但是用户必须注意对象内部的指针和它的清理方式。

7、  不要再析构函数内部触发异常

析构函数会在抛出异常的其他过程中被调用,所以,绝不要再析构函数中抛出异常或者

在析构函数中执行其他可能触发抛出异常的操作。如果析构函数中抛出异常,这个新的异常可能会在现存的异常到达catch子句之前被抛出,这会导致程序程序调用terminate()函数。如果,在析构函数中调用的函数可能会抛出异常,应该在这个析构函数中编写一个try块,并把这些函数调用放到try块中,析构函数必须自己处理所有这些异常。绝对不能有任何一个异常从析构函数中抛出。

8、  避免悬挂指针

如果需要给指针分配资源,那么悬挂指针通常意味着构造函数的弱点。如果在构造函数

中抛出异常,因为指针没有析构函数,那么这些资源将无法释放。请使用auto_ptr或者其他智能指针来处理指向堆的内存。

1.10 使用异常造成的开销

一个throw表达式就像是一个特殊的系统函数调用,它接收异常对象作为参数并且沿着执行调用链向上回溯。为了完成这项工作,需要运行栈的支持。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值