第十七章 用于大型程序的工具
相对于小的程序员团队所能开发的系统需求而言,大规模编程 对程序设计语言的要求更高。大规模应用程序往往具有下列特殊要求:
更严格的正常运转时间以及更健壮的错误检测和错误处理。错误处理经常必须跨越独立开发的多个子系统进行。
能够用各种库(可能包含独立开发的库)构造程序。
能够处理更复杂的应用概念。
C++ 中有下列三个特征分别针对这些要求:异常处理、命名空间和多重继承。
17.1. 异常处理
C++ 的异常处理中,需要由问题检测部分抛出一个对象给处理代码,通过这个对象的类型和内容,两个部分能够就出现了什么错误进行通信。
异常是通过抛出 对象而引发 的。该对象的类型决定应该激活哪个处理代码。被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那个。
异常以类似于将实参传递给函数的方式抛出和捕获。 异常可以是可传给非引用形参的任意类型的对象,这意味着必须能够复制该类型的对象。
传递数组或函数类型实参的时候,该实参自动转换为一个指针。 被抛出的对象将发生同样的自动转换 ,因此,不存在数组或函数类型的异常 。相反。相反,如果抛出一个数组,被抛出的对象转换为指向数组首元素的指针,类似地,如果抛出一个函数,函数被转换为指向该函数的指针第 7.9 节 。
执行 throw 的时候,不会执行跟在 throw 后面的语句,而是将控制从 throw 转移到匹配的 catch ,该 catch 可以是同一函数中局部的 catch ,也可以在直接或间接调用发生异常的函数的另一个函数中。控制从一个地方传到另一地方,这有两个重要含义:
1.沿着调用链的函数提早退出。第 17.1.2 节 将讨论函数因异常而退出时会发生什么。
2.一般而言,在处理异常的时候,抛出异常的块中的局部存储不存在了。
因为在处理异常的时候会释放局部存储,所以被抛出的对象就不能再局部存储 ,而是用 throw 表达式初始化一个称为异常对象 的特殊对象。异常对象由编译器管理,而且保证驻留在可能被激活的任意 catch 都可以访问的空间。 这个对象由 throw 创建,并被初始化为被抛出的表达式的副本。异常对象将传给对应的 catch ,并且在完全处理了异常之后撤销。
异常对象通过复制被抛出表达式的结果创建,该结果必须是可以复制的类型。
异常对象与继承
当抛出一个表达式的时候,被抛出对象的静态编译时类型将决定异常对象的类型。
异常与指针
用抛出表达式抛出静态类型时,比较麻烦的一种情况是,在抛出中对指针解引用。对指针解引用的结果是一个对象,其类型与指针的类型匹配。如果指针指向继承层次中的一种类型,指针所指对象的类型就有可能与指针的类型不同。无论对象的实际类型是什么,异常对象的类型都与指针的静态类型相匹配。如果该指针是一个指向派生类对象的基类类型指针,则那个对象将被分割(第 15.3.1 节 ),只抛出基类部分。
如果抛出指针本身,可能会引发比分割对象更严重的问题 。具体而言,抛出指向局部对象的指针总是错误的,其理由与从函数返回指向局部对象的指针是错误的一样(第 7.3.2 节 )抛出指针的时候,必须确定进入处理代码时指针所指向的对象存在。
如果抛出指向局部对象的指针,而且处理代码在另一函数中,则执行处理代码时指针所指向的对象将不再存在。即使处理代码在同一函数中,也必须确信指针所指向的对象在 catch 处存在。如果指针指向某个在 catch 之前退出的块中的对象,那么,将在 catch 之前撤销该局部对象。
抛出指针通常是个坏主意:抛出指针要求在对应处理代码存在的任意地方存在指针所指向的对象。
17.1.2. 栈展开
如果对抛出异常的函数的调用是在 try 块中,则检查与该 try 相关的 catch 子句。如果找到匹配的 catch ,就处理异常;如果找不到匹配的 catch ,调用函数也退出,并且继续在调用这个函数的函数中查找。
这个过程,称之为栈展开(stack unwinding) ,沿嵌套函数调用链继续向上,直到为异常找到一个 catch 子句。只要找到能够处理异常的 catch 子句,就进入该 catch 子句,并在该处理代码中继续执行。当 catch 结束的时候,在紧接在与该 try 块相关的最后一个 catch 子句之后的点继续执行。
为局部对象调用析构函数
栈展开期间,释放局部对象所用的内存并运行类类型局部对象的析构函数。
如果一个块直接分配资源,而且在释放资源之前发生异常,在栈展开期间将不会释放该资源。例如,一个块可以通过调用 new 动态分配内存,如果该块因异常而退出,编译器不会删除该指针,已分配的内在将不会释放。
析构函数应该从不抛出异常
在为某个异常进行栈展开的时候,析构函数如果又抛出自己的未经处理的另一个异常,将会导致调用标准库 terminate 函数。一般而言,terminate 函数将调用 abort 函数,强制从整个程序非正常退出。
在实践中,因为析构函数释放资源,所以它不太可能抛出异常。标准库类型都保证它们的析构函数不会引发异常。
异常与构造函数
如果在构造函数对象的时候发生异常,则该对象可能只是部分被构造,它的一些成员可能已经初始化,而另一些成员在异常发生之前还没有初始化。即使对象只是部分被构造了,也要保证将会适当地撤销已构造的成员。
类似地,在初始化数组或其他容器类型的元素的时候,也可能发生异常,同样,也要保证将会适当地撤销已构造的元素。
未捕获的异常终止程序