高级程序设计

24 - 异常处理

2020-05-07 14:00 CST
2020-05-09 16:03 CST
CC BY-NC 4.0

异常概述

程序的错误通常包括:

  • 语法错误
  • 逻辑错误/语义错误
  • 运行异常

导致程序运行异常的情况是可以预料的,但它是无法避免的。为了保证程序的鲁棒性(robustness),必须在程序中对可能出现的异常进行预见性处理。

异常处理的策略

  • 就地处理:在发现异常错误的地方处理异常
  • 异地处理:在其他地方处理异常

异常的就地处理

常用做法是调用C++标准库的exitabort终止程序执行。

  • abort立即终止程序的执行,不做任何的善后工作。
  • exit在终止程序的运行前回关闭被程序打开的文件、调用全局对象和static存储类的局部对象的析构函数(不要在析构函数中调用exit)等工作。

就地处理不是user-friendly的处理方式。

异常的异地处理

发现异常时,在发现地有时不知道如何处理这个异常,或者不能很好的处理这个异常,要由程序的其他地方(如函数的调用者)来处理。

一种解决途径是通过函数的返回值,如指针/引用类型的参数或全局变量把异常情况通知函数的调用者,由调用者处理异常。

该途径的不足:

  • 通过函数的返回值返回异常情况导致正常返回值和异常返回值交织在一起,有时无法区分;
  • 通过指针/引用类型的参数返回异常情况,需要引入额外的参数,给函数的使用带来负担;
  • 通过全局变量返回异常情况会导致使用者忽视这个全局变量的问题(使用者不知道它的存在);
  • 程序的可读性差:程序的正常处理和异常处理混杂在一起。

另一种解决异常的异地处理途径是通过语言提供的结构化异常处理机制来进行处理。

C++异常处理机制

  • 把有可能遭遇异常的一系列操作(语句或函数调用)构成一个try语句块。
  • 如果try语句块中的某个操作在执行中发现了异常,则通过执行一个throw语句抛掷(产生)一个异常对象,之后的操作不再进行。
  • 抛掷的异常对象将由程序中能够处理这个异常的地方通过catch语句块来捕获并处理。

try语句块的作用是启动异常处理机制。格式为

try {
  <语句序列>
}

throw语句用于在发现异常情况时抛掷(产生)异常对象,格式为

throw <表达式>;

其中表达式为任意类型的C++表达式(void除外)。

catch语句块用于捕获throw抛掷的异常对象并处理对应的异常。格式为

catch (<类型> [<变量>]) {
  <语句序列>
}
  • <类型>用于指出捕获何种异常对象,它与throw所产生的异常对象的类型匹配规则与函数重载的绑定规则类似;
  • <变量>用于存储异常对象,它可以缺省,缺省时表明catch语句块只关心异常对象的类型,而不考虑具体的异常对象。

catch语句块要紧接在某个try语句的后面。 一个try语句块的后面可以跟多个catch语句块,用于捕获不同类型的异常对象并进行处理。

  • 如果在try语句块的<语句序列>执行中没有抛掷异常对象,则其后的catch语句不执行,而是继续执行try语句块之后的非catch语句。
  • 如果在try语句块的<语句序列>执行中抛掷了异常对象,
    • 如果该try语句块之后有能够捕获该异常对象的catch语句,则执行这个catch语句中的<语句序列>,然后继续执行这个catch语句之后的非catch语句。
    • 如果该try语句块之后没有能够捕获该异常对象的catch语句,则按嵌套的异常处理规则进行处理。

异常处理的嵌套

try语句是可以嵌套的:在try语句块中的语句序列执行过程中还可以包含try语句块。

  • 当在内层的try语句的执行中产生了异常,则首先在内层try语句块之后的catch语句序列中查找与之匹配的处理,如果内层不存在能捕获相应异常的catch,则逐步向外层进行查找。
  • 如果抛掷的异常对象在程序的函数调用链上没有给出捕获,则调用系统的terminate函数进行标准的异常处理。默认情况下,terminate函数将会去调用abort函数。

异常处理实现机制

  • 每个函数都有一个catch表。
  • 每进入一个try,都会把其后的所有catch入口地址记录在相应函数的catch表中。
  • 执行throw时,顺着函数调用链去搜索catch入口;对之前函数调用的栈空间进行退栈处理;并转到搜索到的catch入口。

基于断言的程序调试

一个处于开发阶段的程序可能会有一些错误,通过测试可以发现程序存在错误,通过调试可以对错误进行定位。

除了利用调试工具以外,一种常用的调试手段是输出调试信息,但存在以下问题:

  • 调试者需要对输出的值做一定的分析才能知道程序是否有错;
  • 在开发结束后,去掉调试信息有时是一件很繁琐的工作。

断言

实际上,在调试程序时输出程序在一些地方的某些变量或表达式的值的目的是确认程序运行到这些地方时状态是否正确。

上述目的可以在程序的一些关键或容易出错的点上插入一些断言来表达。

  • 断言(assertion)是一个逻辑表达式,描述了程序执行到断言处应该满足的条件;
  • 如果条件满足则程序继续执行下去,否则程序异常终止。

在程序开发阶段,断言可以用来帮助开发者发现程序的错误,也可以用于错误定位。

assert

C++标准库提供的一个宏assert可以用来实现断言,其格式为

assert(<表达式>);

表达式一般为一个关系/逻辑表达式。

assert执行时,

  • 如果表达式的值为true,程序继续正常执行;
  • 如果表达式的值为false,则会首先显示出相应的表达式、断言所在的源文件和行号等诊断信息;然后调用库函数abort终止程序的运行。

assert是通过条件编译预处理命令来实现的,其实现细节大致如下:

#ifdef NDEBUG
#define assert(exp) ((void)0)
#else
#define assert(exp) ((exp)?(void)0:<输出诊断信息并调用库函数abort>)
#endif

NDEBUG通常在编译命令(开发环境)中定义。