异常处理
异常处理概述什么是异常异常处理(Exception Handling)就地处理异常的异地处理C++的异常处理机制try语句throw语句catch语句异常处理的嵌套基于断言的程序调试断言(assertion)Assert
异常处理概述
什么是异常
- 程序的错误通常包括
- 语法错误
- 逻辑错误:指程序设计不当造成程序没有完成预期的功能
- 运行异常:指程序设计对程序运行环境考虑不周而造成的程序运行错误
- 程序运行环境正常的情况下,运行异常的错误是不会出现的。
- 导致程序运行异常的情况是可以预料的,但它是无法避免的。
- 为了保证程序的鲁棒性(Robustness),必须在程序中对可能出现的异常错误进行预见性处理
异常处理(Exception Handling)
- 异常处理包括: 发现异常 处理异常
- 发现异常 在程序中对可能出现的异常情况进行检测
- 处理异常 就地处理:在发现异常的地方处理异常。 异地处理:在其它地方(非异常发现地)处理异常。
就地处理
- 检测到异常时,往往由于不知道如何针对性地处理这个异常,或者不能很好地处理这个异常,因此,就地处理异常的通常做法是调用C++标准库中的函数
exit或abort终止程序执行(在cstdlib或stdlib.h中声明) - abort立即终止程序的执行,不作任何的善后处理工作
- exit在终止程序的运行前,会做关闭被程序打开的文件、调用全局对象和static存储类的局部对象的析构函数(注意:不要在这些对象类的析构函数中调用exit)等工作
异常的异地处理
- 发现异常时,可由程序的其它地方来更好地处理之。
- 例如,前面的函数f中打开文件失败,这时可以由函数f的调用者重新提供一个文件来解决。
- 如何实现异常的异地处理?
- 一种解决途径:
- 通过函数的返回值,或指针/引用类型的参数,或全局变量把该函数执行中碰到的异常情况通知函数的调用者。
- 函数的调用者根据函数的返回值来处理异常。
- 该途径的不足:
- 通过函数的返回值返回异常情况会导致正常返回值和异常返回值交织在一起,有时无法区分。
- 通过指针/引用类型的参数返回异常情况,需要引入额外的参数,给函数的使用带来负担。
- 通过全局变量返回异常情况会导致使用者会忽略这个全局变量的问题。(不知道它的存在
- 程序的可读性差!程序的正常处理代码与异常处理代码混杂在一起,不能显式地区分它们。
- 另一种解决异常的异地处理途径:通过语言提供的结构化异常处理机制进行处理。
C++的异常处理机制
- 启动异常处理机制
把有可能出现异常的一系列操作(语句或函数调用)放在一个try语句块中。
- 生成异常对象
如果try语句块中的某个操作在执行中发现了异常,则通过执行一个throw语句生成一个异常对象,接在throw之后的操作不再进行。
- 捕获异常对象
生成的异常对象由程序中能够处理这个异常的地方通过catch语句块来捕获并处理之。
try语句
try语句块的作用是启动异常处理机制。格式为:try { <语句序列> }
上述的<语句序列>中可以有函数调用。例如
try { f(str); }throw语句
throw语句用于在发现异常情况时生成异常对象。格式为:
throw <表达式>;注意,这里的表达式不能为void类型
- 执行throw语句后,接在其后的语句将不再继续执行,而是转向异常处理(由某个catch语句给出)
catch语句
- catch语句块用于捕获throw生成的异常对象并处理相应的异常。格式为:
catch (<类型> [<变量>]) { <语句序列> }<类型>用于指出捕获何种异常对象。 <变量>用于存储捕获到的异常对象。它可以缺省,表明catch语句块只关心异常对象的类型,而不考虑具体的异常对象。
- catch语句块要紧接在某个try语句的后面。
- 一个try语句块的后面可以跟多个catch语句块,用于捕获并处理不同类型的异常对象,它们采用精确匹配与throw所产生的异常对象进行绑定。例如:
- 如果在try语句块的<语句序列>执行中没有抛掷(throw)异常对象,则try语句块之后的catch语句不执行,而是继续执行try语句块之后的非catch语句。
- 如果在try语句块的<语句序列>执行中抛掷了(throw)异常对象,
- 如果该try语句块之后有能够捕获该异常对象的catch语句,则执行这个catch语句中的<语句序列>,然后继续执行之后的非catch语句。
- 如果该try语句块之后没有能够捕获该异常对象的catch语句,则按嵌套的异常处理规则进行处理。
异常处理的嵌套
- 在try语句块的语句序列执行过程中还可以包含try语句块。
- 当在内层的try语句的执行中产生了异常,则首先在内层try语句块之后的catch语句序列中查找与之匹配的处理;如果内层不存在能捕获相应异常的catch,则逐步向外层进行查找。
- 如果生成的异常对象在程序的函数调用链上没有给出捕获,则调用系统的terminate函数进行标准的异常处理:
- 默认情况下,terminate函数将会去调用abort函数。
- 注意:有了异常处理机制后,一个函数的对外接口说明: 除了要包含函数的功能描述以及函数的参数和返回值的含义描述之外,还要包含函数可能产生的异常描述。
基于断言的程序调试
- 一个处于开发阶段的程序可能会含有一些逻辑错误或运行异常错误。 通过测试可以发现程序存在的错误。 通过调试可以对错误进行定位。
- 常用的调试手段: 利用调试工具:单步执行、设置断点、......。 在程序中的某些地方加上一些输出语句,在程序运行时把一些调试信息(如变量的值)输出,供调试人员分析。
- 采用输出调试信息的调试手段存在以下问题: 调试者需要对输出的值做一定的分析才能知道程序是否有错。 在开发结束后,去掉调试信息有时是一件很繁琐的工作。
断言(assertion)
- 实际上,在调试程序时输出程序在一些地方的某些变量或表达式的值,其目的是为了确认程序运行到这些地方时状态是否正确。
- 上述目的可以在程序编写时通过在程序的一些关键或容易出错的地方插入一些断言来表达:
- 断言(assertion)是一个逻辑表达式,它描述了程序执行到断言处应满足的条件。
- 如果条件满足则程序继续执行下去,否则程序异常终止。
- 断言的作用:
- 帮助对程序进行理解和形式化验证。
- 在程序开发阶段,帮助开发者发现程序的错误和进行错误定位。
Assert
- C++标准库中提供了一个实现断言的宏assert(在头文件
cassert或assert.h中定义),其格式为:assert(<表达式>);<表达式>一般为一个关系/逻辑表达式
- 执行assert时,
- 如果<表达式>的值为true,程序继续正常执行。
- 如果<表达式>的值为false,则它会:
- 首先,显示出相应的表达式、该assert所在的源文件名以及所在的行号等诊断信息;
- 然后,调用库函数abort终止程序的运行。