用于大型程序的工具
--异常处理
引言:
C++语言包含的一些特征在问题比较复杂,非个人所能管理时最为有用。如:异常处理、命名空间和多重继承。
相对于小的程序员团队所能开发的系统需求而言,大规模编程[往往涉及数千万行代码]对程序设计语言的要求更高。大规模应用程序往往具有下列特殊要求:
1.更严格的正常运转时间以及更健壮的错误检测和错误处理。错误处理经常必须跨越独立开发的多个子系统进行[异常处理]。
2.能够用各种库(可能包含独立开发的库)构造程序[命名空间]。
3.能够处理更复杂的应用概念[多重继承&虚继承]。
异常处理
使用异常处理,程序中独立开发的各部分就能够就程序执行期间出现的问题相互通信,并处理这些问题。程序的一个部分能够检测出本部分无法解决的问题,这个问题检测部分就可以将问题传递给准备处理问题的其他部分。
【注解】
通过异常我们能够将问题的检测和问题的解决分离,这样程序的问题检测部分可以不必了解如何处理问题。
C++的异常处理中,需要由问题检测部分抛出一个对象给处理代码,通过这个对象的类型和内容,两个部分就能够就出现了什么错误进行通信。
如:前面曾经介绍过的一个例子:
Sales_item operator+(const Sales_item &lsh,const Sales_item &rhs) { if (!lsh.same_isbn(rhs)) { throw runtime_error("Data must refer to same ISBN"); } Sales_item ret(lsh); ret += rhs; return ret; }
程序中将Sales_item对象相加的部分可以使用一个try块,以便在异常发生时捕获异常:
Sales_item item1,item2,sum; while (cin >> item1 >> item2) { try { sum = item1 + item2; } catch(const runtime_error &e) { cerr << e.what() << " Try again.\n" << endl; } }
一、抛出类类型的异常
异常是通过抛出对象而引发的。该对象的类型决定应该激活哪个处理代码。被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那个。
异常以类似于将实参传递给函数的方式抛出和捕获。异常可以是可传给非引用形参的任意类型的对象,这意味着必须能够复制该类型的对象。
不存在数组或函数类型的异常。相反,如果抛出一个数组,被抛出的对象转换为指向数组首元素的指针,类似的,如果抛出一个函数,函数转换为指向该函数的指针。
执行throw时,不会执行跟在throw后面的语句,而是将控制从throw转移到匹配的catch,该catch可以是同一函数中局部的catch,也可以在直接或间接基类调用发生异常的函数的另一个函数中。控制从一个地方传到另一地方,这有两个重要含义:
1)沿着调用链的函数提早退出。
2)一般而言,在处理异常的时候,抛出异常的块中的局部存储不存在了。
因为在处理异常的时候会释放局部存储,所以被抛出的对象就不能在局部存储,而是用throw表达式初始化一个称为异常对象的特殊对象。异常对象由编译器管理,而且保证驻留在可能被激活的任意catch都可以访问的空间。这个对象由throw创建,并被初始化为被抛出的表达式的副本。异常对象将传给对应的catch,并且在完全处理了异常之后撤销。
【小心地雷】
异常对象通过复制被抛出表达式的结果创建,该结果必须是可以复制的类型。
1、异常对象与继承
当抛出一个表达式时,被抛出对象的静态编译时类型将决定异常对象的类型。
通常,使用静态类型抛出对象不成问题。当抛出一个异常的时候,通常在抛出点构造将抛出的对象,该对象表示出了什么问题,所以我们知道确切的异常类型。
2、异常与指针
如果指针指向继承层次中的一种类型,指针所指对象的类型就有可能与指针的类型不同。无论对象的实际类型是什么,异常对象的类型都与指针的静态类型相匹配。如果该指针是一个指向派生类对象的基类类型指针,则那个对象将被分割,只抛出基类部分。
谨记:抛出指向局部对象的指针总是错误的,因此,在抛出指针的时候,必须确定进入处理代码时指针所指向的对象存在。
【小心地雷】
抛出指针通常是个坏主意:抛出指针要求在对应处理代码存在的任意地方存在指针所指向的对象。
//P582 习题17.1 range_error r("error"); throw r; //异常对象类型为 range_error exception *p = &r; throw *p; //被异常对象是对指针p进行解引用的结果,其类型与p的静态类型相匹配,为exception
二、栈展开
抛出异常的时候,将暂停当前函数的执行。首先检查throw本身是否在try块内部,如果是,则检查与该try相关的catch子句,看是否其中之一与被抛出对象相匹配。如果找到匹配的catch,就处理异常;如果找不到,就退出当前函数(释放当前函数的内存并撤销局部对象),并且继续在调用函数中查找。
如果对抛出异常的函数的调用是在try块中,则检查与该try相关的catch子句。如果找到匹配的catch,就处理异常;如果找不到匹配的catch,调用函数也退出,并且继续在调用这个函数的函数中查找。
这个过程,称之为栈展开,沿嵌套函数调用继续向上,直至为异常找到一个catch子句。只要找到能够处理异常的catch子句,就进入该catch子句,并在该处理代码中继续执行。当catch结束的时候,在紧接在与该try块相关的最后一个catch子句之后的点继续执行。
1、为局部对象调用析构函数
栈展开期间,提早退出包含throw的函数和调用链中可能的其他函数。在释放内存之前,撤销在异常发生之前所创建的所有对象。如果局部对象是类类型的,就自动调用该对象的析构函数。通常,编译器不撤销内置类型的对象。
【小心地雷】
栈展开期间,释放局部对象所用的内存并运行类类型局部对象的析构函数。
如果一个块直接分配资源,而且在释放资源之前发生异常,在栈展开期间将不会释放该资源。例如,一个块可以通过调用new动态分配内存,如果该块因异常而退出,编译器不会删除该指针,已分配的内在将不会释放。
由类类型对象分配的资源一般会被适当地释放。运行局部对象的析构函数,由类类型对象分配的资源通常由它们的析构函数释放。
2、析构函数应该从不抛出异常
在为某个异常进行栈展开的时候,析构函数如果又抛出自己的未经处理的另一个异常,将会导致调用标准库terminate函数。一般而言,terminate函数将调用abort函数,强制从整个程序非正常退出。
因为terminate函数结束程序,所以析构函数做任何可能导致异常的事情通常都是非常糟糕的主意。在实践中,因为析构函数释放资源,所以它不太可能抛出异常。标准库类型都保证它们的析构函数不会引发异常。
3、异常与构造函数
构造函数内部所作的事情经常会抛出异常。在构造函数内部,即使对象只是部分被构造了,也要保证将会适当的撤销已构造的成员。
类似地,在初始化数组或其他容器类型的元素的时候,也可能发生异常,同样,也要保证将会适当地撤销已构造的元素。
4、未捕获的异常终止程序
不能不处理异常。异常是足够重要的、使程序不能继续正常执行的事件。如果找不到匹配的catch,程序就调用库函数terminate[你懂得。。。]!
三、捕获异常
catch子句中的异常说明符看起来像只包含一个形参的形参表,异常说明符是在其后跟一个(可选)形参名的类型名。
说明符的类型决定了处理代码能够捕捉的异常种类。类型必须是完全类型,即必须是内置类型或者是已经定义了的程序员自定义的类型。类型的前向声明不行。
当catch为了处理异常只需要了解异常的类型的时候,异常说明符可以省略形参名;如果处理代码需要已发生异常的类型之外的信息,则异常说明符就包含形参名,catch使用这个名字访问异常对象。
1、查找匹配的处理代码
在查找匹配的catch期间,找到的catch不必是与异常最匹配的那个,相反,将选中第一个找到的可以处理该异常的catch。因此,在catch子句列表中,最特殊的catch必须最先出现。
异常与catch异常说明符匹配:大多数转换都不允许 --除下面几种可能的区别之外,异常的类型与catch说
明符的类型必须完全匹配:
1)允许从非const到const的转换。也就是说,非const对象的 throw可以与指定接受const引用的 catch匹配。
2)允许从派生类型型到基类类型的转换。
3)将数组转换为指向数组类型的指针,将函数转换为指向函数类型的适当指针。
在查找匹配catch的时候,不允许其他转换。具体而言:既不允许标准算术转换,也不允许为类类型定义的转换[好绝情%>_<%]。
2、异常说明符
进入catch的时候,用异常对象初始化catch的形参。像函数形参一样,异常说明符类型可以是引用。异常对象本身是被抛出对象的副本。是否再次将异常对象复制到catch位置取决于异常说明符类型。
如果说明符不是引用,就将异常对象复制到catch形参中,对形参所做的任何改变都只作用于副本,不会作用于异常对象本身。如果说明符是引用,则像引用形参一样,不存在单独的catch对象,
catch形参只是异常对象的另一名字。对catch形参所做的改变作用于异常对象。
3、异常说明符与继承
像形参声明一样,基类的异常说明符可以用于捕获派生类型的异常对象,而且,异常说明符的静态类型决定catch子句可以执行的动作。如果被抛出的异常对象是派生类类型的,但由接受基类类型的catch处理,那么,catch不能使用派生类特有的任何成员。
【最佳实践】
通常,如果catch子句处理因继承而相关的类型的异常,它就应该将自己的形参定义为引用。此时catch对象的静态类型可以与catch对象所引用的异常对象的动态类型不同。
如果catch对象是基类类型对象而异常对象是派生类型的,就将异常对象分割为它的基类子对象。
对象(相对于引用)不是多态的。对象的静态类型和动态类型相同,函数是虚函数也一样。只有通过引用或指针调用时才发生动态绑定,通过对象调用不进行动态绑定。
4、catch子句的次序必须反映类型层次
将异常类型组织成类层次的时候,用户可以选择应用程序处理异常的粒度级别。例如,只希望清除并退出的应用程序可以定义一个try块,该try块包围main函数中带有如下catch代码:
catch(exception &e) { cerr << "Exiting: " << e.what() << endl; size_t status_indicator = 42; return(status_indicator); }
有更严格实时需求的程序可能需要更好的异常控制,这样的应用程序将清除导致异常的一切并继续执行。
因为catch子句按出现次序匹配,所以使用来自继承层次的异常的程序将它们的catch子句排序,以便派生类型的处理代码出现在其基类类型的catch之前。
【注解】
带有因继承而相关的类型的多个catch子句,必须从最低层派生类型到最高派生类型排序。如下题:
//P585 习题17.3 try { //... } catch(overflow_error eobj) { //... } catch(const runtime_error &re) { //... } catch(exception) { }