第十七章:用于大型程序的工具:异常处理
现对于小型的软件系统,大规模编程对程序设计语言和程序员的要求更高,它们往往具有以下要求:
1:更严格的正常运转时间以及更强壮的错误检测和错误处理。
2:运用各种库进行开发。
3:能够处理更复杂的应用概念。
C++中所具有的异常处理、命名空间和多重继承可以很好的满足这些要求。
一:异常处理
使用异常处理,程序中独立开发的各部分能够就程序执行期间出现的问题相互通信,并处理这些问题。这能够将问题的检测和问题的解决分离,可以使程序员更专心实现程序的主要逻辑功能。
异常是通过抛出(throw)异常对象而引发的。该对象的类型决定应该激活哪个处理代码。被选中的处理代码是调用链中,与该对象类型匹配且离抛出位置最近的那个。
异常类似于将实参传递给函数的方式抛出和捕获,它是可以传给非引用形参的任意类型的对象,这意味着必须能够复制该类型的对象。
不存在数组或函数类型的异常,它们都会被转换为指针。这也意味着一般的对象均可以被抛出。
执行throw时,不会执行throw之后的语句,而是将控制从throw转移到匹配的catch。该catch可以是同一函数中局部的catch,也可以在直接或间接调用发生异常的另一个函数中。在处理异常时局部存储会被释放,因此被抛出的对象就不能在局部存储,而是用throw表达式初始化一个被抛出异常对象的副本。异常对象由编译器管理并且保证驻留在可能被激活的任意catch都可以访问的空间。在处理完异常后,异常对象将会被撤销。
许多应用程序抛出的异常都来自某个继承层次。当抛出一个异常时,被抛出对象的静态编译类型决定异常的类型。如要抛出对指针的解引用时,无论指针是什么类型,异常对象的类型都跟指针的类型相匹配。如果指针是指向派生类对象的基类指针,则那个对象将会被分割,只抛出基类部分。
如果抛出的是局部对象的指针,会导致比对象分割更为严重的问题。因此抛出指针通常是个坏主意。
抛出异常时将暂停当前函数执行,开始查找匹配catch子句。首先检查throw是否在catch块内部,如果是,检查该try相关的catch子句看是否与其中一个catch对象相匹配。如果找到匹配的catch,就处理异常。如果没找到,就退出当前函数,并继续在调用函数中查找。这个过程被称为堆栈展开。
栈展开期间,会释放局部对象所用的内存并运行类的析构函数。
如果一个块从堆中分配资源,而在释放之前发生了异常,则从堆中分配的内存不会被释放。因此使用类管理从堆中分配的对象并在析构函数中释放是一个很好的处理方式。在发生异常后,类的析构函数肯定会被调用,用于释放资源。这就保证了资源的安全使用。
栈展开期间会经常执行析构函数。在执行析构函数时,已经引发了异常但还没有处理它,如果在这时候析构函数本身又抛出新的异常将会导致调用标准库的terminate函数。该函数会调用abort函数,强制整个程序非正常退出。在实际中,析构函数主要是为释放资源,所以它不太可能会抛出异常。
构造函数经常会抛出异常。如果在构造函数中发生了异常,则对象可能只是部分被构造,它的一些成员可能已经初始化,某些资源可能已经分配,因此要适当的撤销已构造的成员,释放已分配的资源。
类似的在构造容器或数组时,也要保证在发生异常时适当的撤销已构造的元素。
异常是足够重要的、使程序不能继续正常执行的事件,如果找不到匹配的catch,程序就调用terminate函数,强制终止程序。
异常说明符是catch后跟的是一个形参的类型名。异常说明符的类型决定处理代码能够捕获的异常种类。类型必须为完整类型,即必须是内置类型或是已经定义的自定义类型。仅有类的声明是不行的。
当catch只需要知道异常类型,而不需要访问异常对象时,形参名可以省略。如果需要异常类型之外的信息,则必须包含形参名。
异常与catch异常说明符的匹配规则,比匹配实参和形参类型规则更为严格。除了几种可能的转换之外,大多数转换都不允许:
1:允许从非const到const的转换。
2:允许从派生类型到基类类型的转换。(分割)
3:数组转换为指向数组类型的指针,函数转换为适当的指针。
除此之外,不允许其他转换,既不允许标准算术转换,也不允许为类类型定义的转换。(转换函数)
进入catch时,使用异常对象初始化catch的形参。像函数形参一样,异常说明符可以使引用,也可以是传值。这与函数传参效果相同。
异常说明符的静态类型,决定catch子句可以执行的动作。如果抛出的异常对象(非引用)是派生类类型的,但由接受基类类型的catch处理,那么将发生分割,catch不能使用派生类特有的任何成员。
如果catch形参为引用类型,异常对象的的静态类型可以与catch对象所引用的动态类型不同。通过引用调用时才发生动态绑定,通过对象调用不发生动态绑定。(注意与上面介绍的指针的解引用区别开)
catch子句处理因继承而相关的类型的异常,它就应该将自己的形参定义为引用,基类的引用会处理所有派生类类型的异常。
catch子句按出现的次序匹配异常对象,因此必须对子句进行排序,以便派生类型的处理代码出现在其基类类型的catch之前。
catch可以通过重新抛出,将异常传递给函数调用链更上层的函数。重新抛出语句为:throw;
空语句代表重新抛出异常对象。它只能出现在catch或catch调用的函数中。如果在其他地方遇到空的throw语句,就调用terminate函数。
程序可以捕获所有异常,捕获所有异常的catch子句形式为(...).
例如:
catch(...){}
捕获所有异常的catch子句与任意类型的异常都匹配。
如果捕获所有异常与其他catch子句结合使用,它必须是最后一个,否则任何在它后面的catch子句都不能被匹配。
构造函数函数体内部的catch子句不能处理初始化列表中的异常,为了处理来自初始化列表的异常,必须为构造函数编写函数测试块。如:
class Bar
Bar(String s)
try :ptr(NULL),ss(s)
{
}catch()
{
}
};
通过值(非指针)抛出异常,通过引用捕获异常是配合最好的组合。
C++标准库定义了一组类,用于报告在标准库的函数中遇到的问题,程序员可在自己编写的程序中使用这些标准异常类。
1:exception头文件定义了最常见的异常类,是所有异常类的基类。
2:stdexcept头文件定义了几种常见的异常类,它们为:
exception最常见的问题。
run_time_error运行时错误:仅在运行才能检测到问题。
range_error运行时错误:越界。
overflow_error运行时错误:上溢。
underflow_error运行时错误:计算下溢。
logic_error逻辑错误:可在运行前检测到的问题。
domain_error逻辑错误:参数的结果值不存在。
invalid_argument逻辑错误:不合适的参数。
length_error逻辑错误:超出该类最大长度。
out_of_range逻辑错误:使用一个超出有效范围的值。
3:new头文件定义了bad_alloc异常类型。提供因无法分配内存而由new抛出异常。
4:type_info头文件定义了bad_cast异常类型。转制异常。
标准库异常只提供很少的操作,包括创建、赋值、复制异常类对象。exception、bad_alloc以及bad_cast只定义了默认构造函数,无法在创建这些类型时为它们提供初值。其他类型的异常类型则只定义了一个使用string初始化式的构造函数,当需要定义这些类型的对象时,必须提供一个string参数,用于为发生的错误提供更多的信息。
what虚成员函数是在exception定义的唯一成员函数,该函数返回const*char对象,用于返回在抛出位置构造异常对象的信息。
可以和使用标准库相同的方法使用自己的异常类,程序的一部分抛出某个对象,另一部分捕获并处理。
不要被思维定势迷惑:认为只能抛出标准库提供的异常对象或者在这些类基础上自定义的派生类。其实完全可以抛出自己的定义的类对象,而与标准库没有任何关系。但在标准库基础之上定义可以减少很多工作量。
通过定义一个类来封装资源的分配和释放,可以保证正确释放资源,这一技术常称为资源非配即初始化。
auto_ptr被称为自动指针.它是一个类模板。为动态分配的对象提供异常安全。在memory头文件中定义。
它的构造函数被声明为explicit,因此不能使用auto_ptr<int> ai=new int(20);必须使用auto_ptr<int> ai(new int(20));
auto_ptr只能用于管理从new返回的一个对象,它不能管理动态分配的数组。当auto_ptr被赋值或复制时,它原来所关联的对象就被删除。如ap1=ap2.
ap2将所有权转让给ap1,ap2成为未绑定的auto_ptr对象。
不能直接将一个地址赋给auto_ptr对象,必须调用reset函数来改变指针。
不要将auto_ptr对象保存在容器中,容器要求所保存的类型定义赋值和赋值操作。而auto_ptr不满足这个要求。
异常说明指定函数是否抛出异常以及会抛出哪种异常。它跟在函数形参表之后,在关键字throw跟着一个由圆括号括住的异常类型列表:
Void func()throw (run_time_error);它指明如果该函数抛出异常,则该异常将是run_time_error对象或者其派生类对象。
异常说明是函数接口的一部分,函数定义以及该函数的任意声明必须具有相同的异常说明。如果一个函数声明没有指定异常说明,则该函数可以抛出任意类型的异常。空列表指出函数不抛出任何异常。注意与throw;空语句相区别。(重新抛出异常)
如果函数抛出了没有在其异常说明中列出的异常,就调用标准库函数unexpected。该函数会调用terminate函数。但是这必须在运行时才能检测到,编译时编译器不能也不会试图验证异常说明。
类的成员函数也可以指定异常说明。在const成员函数中,异常说明长跟在const限定符之后。
基类虚函数的异常说明可以与派生类对应的函数异常声明不同。但是派生类虚函数抛出的异常说明,必须与对应基类虚函数的异常说明同样严格或者比它更受限。通俗的说就是,当使用基类类型的指针调用派生类的虚函数时,派生类的异常说明不会增加新的可抛出对象。基类中的异常列表是虚函数的派生类版本可以抛出的异常列表的超集。
异常说明是函数声明的一部分,因此可以在函数指针的定义中提供异常说明。用到时自己再去看吧。P598