第十章 异常处理编写大型和十分复杂的程序,往往会产生一些很难查找的错误。面向对象的异常处理
( exception handling)机制是 C++语言用以解决这个问题的有力工具。函数执行时,放在 try
(测试)程序块中的任何类型的数据对象发生异常,都可被 throw块抛出,随即沿调用键退回,
直到被 catch块捕获,并在此执行异常处理,报告出现的异常等情况。从抛出到捕获,应将各嵌套调用函数残存在栈中的自动对象、自动变量和现场保护内容等进行清除。如果已退到入口函数还未捕获则由 abort()来终结入口函数。
第十章 异常处理
10.1 异常的概念
10.3 捕获异常
10.2 异常处理的机制
10.5 异常规范
10.7 C++标准库的异常类层次结构
10.6 异常和继承
10.4 异常的重新抛出和 catch_all子句
10.1 异常的概念大的软件不可能没有错误,也不可能在排除所有的错误后才投入使用。找出所有潜在的运行时错误那几乎是不可能的,能够做到的是预计可能发生什么类型的错误,并在错误发生时停止发生错误的操作,对它进行处理,而程序的其它部分仍然继续运行。
这里所讲的异常( exception)是程序可能检测到的,运行时不正常的情况,如存储空间耗尽、数组越界、被 0除等等,可以预见可能发生在什么地方,但是无法确知怎样发生和何时发生。特别在一个大型的程序(软件)中,程序各部分是由不同的小组编写的,
它们由公共接口连起来,错误可能就发生在相互的配合上,也可能发生在事先根本想不到的个别的条件组合上。然后由预先安排的程序段来捕获( catch)异常,并对它进行处理。这种机制可以在
C++程序的两个无关(往往是独立开发)的部分进行“异常”通信。
10.2 异常处理的机制首先,在 C++中异常往往用类 ( class) 来实现,异常类的声明如下:
class popOnEmpty{...};
class pushOnFull{...};
template <typename T>void Stack<T>::Push(const T&data){
if(full()) throw pushOnFull(T data);//注意加了括号,是构造一个无名对象
elements[++top]=data;
}
template<typename T>T stack<T>::Pop(){
if(empty()) throw popOnEmpty();
return elements[top--];
}
注意 pushOnFull是类,C++要求抛出的必须是对象,所以必须有,()”,这样是调用构造函数建立一个对象 。 异常并非总是类对象,throw表达式也可以抛出任何类型的对象,如枚举,
整数等等 。 throw表达式为异常处理的第一步 。
10.2 异常处理的机制在 C++中建立异常抛出与异常处理之间有一整套程序设计的机制。
请看下面的程序段给出 try块与 catch字句的关系 。
int main(){ int a[9]={1,2,3,4,5,6,7,8,9},b[9]={0},i;
stack<int>istack(8);
try{ for(i=0;i<9;i++)istack.Push(a[i]);
istack.PrintStack(); }
catch(pushOnFull){ cerr<<”栈满,<<endl; }
try{ for(i=0;i<9;i++){ b[i]=istack.Pop(); } }
catch(popOnEmpty){ cerr<<”栈空,<<endl; }
for(i=0;i<9;i++)cout<<b[i]<<’\t’;
cout<<endl;
return 0;
}
10.2 异常处理的机制程序按下列规则控制:
1,如果没有异常发生,继续执行 try块中的代码,与 try块相关联 的 catch子句被忽略,程序正常执行,main()返回 0。
2,当第一个 try块在 for循环中抛出异常,则该 for循环退出,
try 块 也 退 出,去执行 pushOnFull 异常的 catch 子句 。
istack.PrintStack()不再执行,被忽略 。
3,如果第二个 try块调用 Pop()抛出异常,则退出 for和 try块,
去执行 popOnEmpty异常的 catch子句 。
4,当某条语句抛出异常时,跟在该语句后面的语句将被跳过。程序执行权交给处理异常的 catch子句,如果没有 catch子句能够处理异常,则交给 C++标准库中定义的 terminate()。
10.2 异常处理的机制在编制程序中有一条惯例:把正常执行的程序与异常处理两部分分隔开来,这样使代码更易于跟随和维护。
把程序的正常处理代码和异常处理代码分离的最清楚的方法是定义函数 try
块 ( function try block) 。 这种方法是把整个函数包括在 try块中:
int main() try{
int a[9]={1,2,3,4,5,6,7,8,9},b[9]={0},;
stack <int>istack(8);
......;
return 0;
}
catch(popOnEmpty){cerr<<”栈空,<<endl;return 1;}
catch(pushOnFull){cerr<<”栈满,<<endl;return 2;}
10.2 异常处理的机制最后强调,catch子句必须在 try块之后;而 try块后必须紧跟一个或多个
catch子句,目的是对发生的异常进行处理。 catch的括号中只能有一个类型,当类型与抛掷异常的类型匹配时,称该
catch子句捕获了一个异常,并转到该块中进行异常处理。
10.3 捕获异常
catch子句由三部分组成:关键字 catch、圆括号中的异常声明
( exception declaration)以及复合语句中的一组语句。 catch子句可以包含返回语句( return),也可不包含返回语句。包含返回语句,
则整个程序结束。而不包含返回语句,则执行 catch列表之后的下一条语句。
异常声明中也可以是一个对象声明 。 还是以栈为例 。 pushOnFull类可如下定义:
Template <typename T>class pushOnFull{
T _value;
public:
pushOnFull(T i):_value(i){}
//或等效写为 pushOnFull(int i){value=i;}
T value(){return _value;}
};
10.3 捕获异常新的私有数据成员 _value保存那些不能被压入栈中的值 。 该值即调用构造函数时的实参 。 对应在 throw表达式中,构造抛出对象也要有实参:
throw pushOnFull(data);//data即 Push(const &data)中的参数 data
这样在 catch子句中,要取得 _value,须调用 pushOnFull中的成员函数
value():
catch( pushOnFull<T> eObj) {
cerr<<”栈满,<<eObj.value()<<”未压入栈,<<endl;
return 1;
}
在 catch子句的异常声明中声明了对象 eObj,用它来调用 pushOnFull类的对象成员函数 value()。异常对象是在抛出点被创建,与 catch子句是否显式要求创建一个异常对象无关,该对象总是存在,在 catch子句中只是为了调用异常处理对象的成员函数才声明为对象,不用类。
10.3 捕获异常
catch子句的异常声明,函数参数声明类似,可以是按值传送,也可以是按引用传递 。 如果 catch子句的异常声明改为引用声明,则 catch子句可以直接引用被 throw表达式创建的异常对象,而不必创建自己的局部拷贝 。 对于类类型的异常,其异常声明最好被声明为引用 。
catch(pushOnFull<T> & eObj){
cerr<<” 栈满,<<eObj.value()<<” 未压栈,<<endl;
return 1;
}
10.3 捕获异常寻找匹配的 catch子句有固定的过程:如果 throw
表达式位于 try块中,则检查与 try块相关联的 catch子句列表,看是否有一个子句能够处理该异常,如果有匹配的,则该异常被处理,如果找不到匹配的 catch子句,
则在主调函数中继续查找。如果一个函数调用在退出时带有一个被抛出的异常,而且这个调用位于一个 try块中,
则检查与该 try块相关联的 catch子句列表,看是否有一个子句匹配,如果有,则处理该异常,如果没有,则查找过程在该函数的主调函数中进行。这个查找过程逆着嵌套的函数调用链向上继续,直到找到处理该异常的
catch子句。只要遇到第一个匹配的 catch子句,就会进入该 catch子句,进行处理,查找过程结束。
10.3 捕获异常在栈异常处理的例子中,对 popOnEmpty,首先应在 istack的成员函数 Pop()中找,因为 Pop()中的 throw
表达式没有在 try块中,所以 Pop()带着一个异常退出 。
下一步是检查调用 Pop()的函数,这里是 main(),在
main()中对 Pop()的调用位于一个 try块中,则可用与该
try块关联的 catch子句列表中的某一个来处理,找到第一个 popOnEmpty类型异常声明的子句,并进入该子句进行异常处理 。
在这一过程中,因发生异常而逐步退出复合语句和函数定义,被称为栈展开( stack unwinding)。随着栈展开,在退出的复合语句和函数定义中声明的局部变量的生命期也结束了。
10.3 捕获异常在退出的域中有某个局部量是类对象,栈展开过程将自动调用该对象的析构函数,完成资源的释放。所以
C++异常处理过程本质上反映的是“资源获取是由构造函数实现,而资源释放是由析构函数完成”这样一种程序设计技术。采用面向对象的程序设计,取得资源的动作封装在类的构造函数中,释放资源的动作封装在类的析构函数中,当一个函数带着未处理的异常退出时,函数中这种类对象被自动销毁,资源(包括动态空间分配的资源和打开的文件)释放。栈展开过程决不会跳过封装在类的析构函数中的资源释放动作。
10.3 捕获异常异常对象是在 throw表达式中建立并抛出。步骤:
throw表达式通过调用异常类的构造函数创建一个临时对象,然后把这个临时对象拷贝到一个被称为异常对象
( exception object)的存贮区中,它保证会持续到异常被处理完 。 异常不能够保持在未被处理的状态。异常表示一个程序不能够继续正常执行,这是非常严重的问题,如果没有找到处理代码,程序就调用 C++标准库中定义的函数 terminate()。 terminate()的缺省行为是调用
abort(),指示从程序中非正常退出。
10.3 捕获异常对比函数调用和异常处理之间的异同。 throw表达式的行为有点像函数的调用,而 catch子句有点像函数定义。函数调用和异常处理的主要区别是:建立函数调用所需要的全部信息在编译时已经获得,而异常处理机制要求运行时的支持。与运行时的多态 —— 虚函数也是不一样的。
10.4 异常的重新抛出和 catch_all子句当 catch语句捕获一个异常后,可能不能完全处理异常,完成某些操作后,该异常必须由函数链中更上级的函数来处理,这时 catch子句可以重新抛出 ( rethrow)
该异常,把异常传递给函数调用链中更上级的另一个
catch子句,由它进行进一步处理 。 rethrow表达式为:
throw;
但是重新抛出异常的 catch子句应该把自己做过的工作告诉下一个处理异常的 catch子句,往往要对异常对象做一定修改,以表达某些信息,因此 catch子句中的异常声明必须被声明为引用,这样修改才能真正做在异常对象自身中,而不是拷贝中。
10.4 异常的重新抛出和 catch_all子句通常异常发生后按栈展开 ( stack unwinding)
退出,动态分配的非类对象资源是不会自动释放的,
应该在对应的 catch子句中释放 。 因为我们不知道可能被抛出的全部异常,所以不是为每种可能的异常写一个 catch子句来释放资源,而是使用通用形式的
catch子句 catch_all,格式为:
catch(...){代码 */}
10.4 异常的重新抛出和 catch_all子句见下例:
void fun1(){
int *res;
new res[100];//定义一个资源对象
try{//代码包括使用资源 res和某些可能引起异常抛出的操作
}
catch(...){
delete res[];//释放资源对象 res;
throw;//重新抛出异常
}
delete res[];//正常退出前释放资源对象 res;
}
catch_all子句可以单独使用,也可以与其它 catch子句联合使用 。 如果联合使用,它必须放在相关 catch子句表的最后 。
10.5 异常规范异常规范 ( exception specification) 提供了一种方案,可以随着函数声明列出该函数可能抛出的异常,并保证该函数不会抛出任何其他类型的异常,在 stack类定义中可有:
void Push(const T&data) throw(puthOnFull);
T Pop() throw(popOnEmpty);
一个函数的异常规范的违例只能在运行时才能被检测出来 。 如果在运行时,函数抛出了一个没有被列在它的异常规范中的异常时,
则系统调用 C++ 标 准 库 中 定 义 的 函 数 unexpected() 。
unexpected()缺省操作是调用 teminate()。
异常规范只是在函数所抛出的异常,没有在该函数内部处理,
而是沿调用链回溯寻找匹配的 catch子句时才起作用,
10.5 异常规范
【 例 10.1】 现在可以给出包含栈满异常的较完整的程序段。
异常处理主要是服务于大型程序的,抛出异常和处理异常可以是由不同开发小组完成。
10.6 异常和继承当类的层次结构用于异常时,异常处理的方式变得更加多样化 。
在 C++程序中,表示异常的类通常被组成为一个组 ( group)
( 即如在前面各节讨论的那样 ) 或者一个层次结构 。 对由栈类成员函数抛出的异常:
class popOnEmpty{...};
class pushOnFull{...};
可以定义一个称为 Excp的基类,再从该基类派生出这两个异常类 。
class Excp{...};
class popOnEmpty:public Excp{...};
class pushOnFull:public Excp{...};
10.6 异常和继承由基类 Excp来打印错误信息:
class Excp{
public:
void print(string msg){cerr<<msg<<endl;}
};
这样的基类也可以作为其他异常类的基类:
class Excp{...};//所有异常类的基类
class stack Excp:public Excp{...};//栈异常类的基类
class popOnEmpty:public stackExcp{...};//栈空退栈异常
class pushOnFull:public stackExcp{...};//栈满压栈异常
class mathExcp:public Excp{...};//数学库异常的基类
class zeroOp:public mathExcp{...};//数学库零操作异常
class divideByZero:public mathExcp{...};//数学库被零除异常
10.6 异常和继承这里被创建的异常类对象是 stackExcp类类型,尽管 pse指向一个实际类型为 pushOnFull的对象,但那是一个临时对象,拷贝到异常对象的存贮区 中 时 创 建 的 却 是 stackExcp 类的异常对象 。 所 以 该 异 常 不 能 被
pushOnFull类型的 catch子句处理 。
在处理类类型异常时,catch子句的排列顺序是非常重要的 。 当异常被组织成类层次结构时,类类型的异常可以被该类类型的公有基类的
catch子句捕获到 。 如 pushOnFull类类型的异常可以由 stackExcp或 Excp
类类型异常所对应的 catch子句处理 。 为了保证 pushOnFull异常的处理由最合适的 catch子句来处理,应有如下顺序:
catch(pushOnFull){...}//处理 pushOnFull异常
catch(stackExcp){...}//处理栈的其他异常
catch(Excp){...}//处理一般异常派生类类型的 catch子句必须先出现,以确保只有在没有其他 catch子句适用时,才会进入基类类型的 catch子句。
10.6 异常和继承类层次结构的异常同样可以重新抛出 ( rethrow),
把一个异常传递给函数调用列表中,更上层的另一个
catch子句 。 形式仍为
throw;
虚函数是类层次结构中多态性的基本手段,异常类层次结构中也可以定义虚拟函数。
10.6 异常和继承
【 例 10.2】 异常层次结构中的虚函数 。
class Excp{
public:
virtual void print(){cerr<<”发生异常,<<endl;}
};
class stackExp:public Excp{
public:
virtual void print(){cerr<<”栈发生异常,<<endl;}
};
class pushOnFull:public stackExcp{
public:
virtual void print(){cerr<<”栈满,不能压栈,<<endl;}
};
10.6 异常和继承
int main(){
try{//抛出一个 pushOnFulll异常
}
catch(Excp&eObj){
eObj.print();//调用虚函数 pushOnFull::print()
}
}
10.6 异常和继承对异常规范 ( exception specification) 首先,
异常规范可以在类成员函数后面指定,与非成员函数一样,成员函数声明的异常规范也是跟在函数参数表的后面。如果成员函数被声明为 const或 volatile成员函数,则异常规范跟在函数声明的 const和 volatile限定修饰符之后。
第二,如果成员函数在类体外定义,则定义中所指定的异常规范,必须与类定义中该成员函数声明中的类异常规范相同,也就是必须在两处都有相同的异常规范,注意这和函数参数缺省值只能在一处说明
(通常在声明中)不同。
10.6 异常和继承第三,虚函数不同 。 基类中的虚函数的异常规范,可以与派生类改写的虚函数的异常规范不同 。 但这不同指的是派生类的虚拟函数的异常规范必须与基类虚函数的异常一样或更严格 ( 是基类虚函数的异常的子集 ) 。 如:
class CBase{
public:
virtual int fun1(int) throw();
virtual int fun2(int) throw(int);
virtual string fun3() throw(int,string);
};
class CDerived:public CBase{
public:
int fun1(int) throw(int);//错 ! 异常规范不如 throw()严格
int fun2(int) throw(int);//对 ! 有相同的异常规范
string fun3() throw(string);//对!异常规范比
throw(int,string)更严格
10.7 C++标准库的异常类层次结构
C++标准库提供了一个异常类层次结构,用来报告 C++标准库中的函数执行期间遇到的程序不正常情况 。 这些异常类也可以被用在用户编写的程序中,或被进一步派生来描述程序中的异常 。
C++标准库中的异常层次的根类被称为 exception,定义在库的头文件
<exception>中,它是 C++标准库函数抛出的所有异常类的基类 。 exception类的接口如下:
namespace std{//注意在名字空间域 std中
class exception{
public:
exception() throw();//缺省构造函数
exception(const exception &) throw();//拷贝构造函数
exception &operator=(const exception&) throw();//拷贝赋值操作符
virtual ~exception() throw();//析构函数
virtual const char*what() const throw();//返回一个 C风格的字符串
}; }
10.7 C++标准库的异常类层次结构
C++标准库还提供了一些类,可用在用户编写的程序中,以报告程序的不正常情况 。 这些预定义的错误被分为两大类:逻辑错误 ( logic error) 和运行时错误
( run_time error) 。
逻辑错误是那些由于程序的内部逻辑而导致的错误或者违反了类的不变性的错误,
两者都是逻辑错误 。 在 C++标准库中定义的逻辑错误如下:
namespace std{
class logic_error:public exception{
public:
explicit logic_error(const sting &what_arg);// 关键字
explicit取消构造函数隐式转化
};
class invalid_argment:public logic_error{
public:
explicit invalid_argument(const sting &what_arg);
};
10.7 C++标准库的异常类层次结构
class out_of_range:public logic_error{
public:
explicit out_of_range(const string &what_arg);
};
class length_error:public logic_error{
public:
explicit length_error(const string &what_arg);
};
class domain_error:public logic_error{
public:
explicit domain_error(const string &what_arg);
};
}
10.7 C++标准库的异常类层次结构与此相对,运行时刻错误是由于程序域之外的事件而引起的错误 。 运行时刻错误只在程序执行时才是可检测的 。 在 C++标准库中定义的运行时刻错误如下:
namespace std{
class runtime_error:public exception{
public:
explicit runtime_error(const string &what_arg);
};
class range_error:public runtime_error{
public:
explicit range_over(const string &what_arg);
};
10.7 C++标准库的异常类层次结构
class overflow_error:public runtime_error{
public:
explicit overflow_error(const string &what_arg);
};
class underflow_error:public runtime_error{
public:
explicit underflow_error(const string &what_arg);
};
class bad_alloc,public exception {
public:
bad_alloc(const char *_S = "bad allocation") throw();
};
}
10.7 C++标准库的异常类层次结构
【 例 10.3】 为类模板 Array重新定义 operator[](),如果索引值越界,那么它会抛出一个 out_of_range类型的异常:
为了使用预定义的异常类,我们的程序必须包含头文件
<stdexcept>。 传递给 out_of_range构造函数的 string对象
eObj描述了被抛出的异常 。 当该异常被捕获到时,通过
exception类的 what()成员函数可以获取这些信息,
有了这份实现,函数 arr.iuput()中的越界索引值将导致
Array的 operator[]()抛出一个 out_of_range类型的异常,它将在 main()中被捕获到 。
程序,Ex10_3.cpp