程序有时会遇到运行阶段错误,导致程序无法正常的运行下去。如下图所示:
遇到这种问题我们一般都是断点调试或者单步调试。但是,如果有一个较大的循环,将会导致断点调试或单步调试很复杂,因此,学习必要的异常处理找出问题所在还是很有必要的。
assert()函数
编写代码时,我们总是会做出一些假设,断言assert就是用于在代码中捕捉这些假设,可以将断言看作是异常处理的一种高级形式。使用断言可以创建更稳定,品质更好且不易出错的代码。当需要一个值为FALSE时中断操作的话,可以使用断言。
assert宏的原型定义在<assert.h>,其作用是如果它的条件返回错误,则终止程序执行,原型定义如下:
#include<assert.h>
void assert(int expression);
assert的作用是现计算表达式expression,如果其值为假(即为0),那么它先向stderr打印一条出错信息,然后通过调用abort来终止程序运行。从这可以看出,每个assert只检测一个条件,因为同时检测多个条件时,如果断言失败,无法直观的判断是哪个条件失败。
比较好的在程序中使用assert的地方:
(1)空指针检查。例如,针对一个函数的参数进行空指针检查。我们可以这样使用:assert(pointer != NULL),产生的错误会像这样:Assertion ‘pointer != ((void *)0)’ failed。这样,当出现空指针时,我们的程序就会退出,并很好的给出错误信息;
(2)检查函数参数的值。例如,,如果一个函数只能在它的一个参数foo为正值的时候被调用,我们可以在函数开始时这样写:assert(foo > 0),这将帮助我们检测函数的错误使用,这也源代码阅读者很清晰的印象,那就是在这里对函数的参数值有限制。
使用assert()的缺点是,频繁的调用会极大的影响程序的性能,增加额外的开销。在调试结束后,可以通过包含#include<assert.h>的语句之前插入#define NDEBUG来禁用assert调用。assert只有在Debug版本中才有用,如果编译为Release版本则被忽略。
abort()
abort()函数的原型位于头文件cstdlib(或stdlib.h),其中典型实现是向标准错误流(即cerr使用的错误流)发送消息abnormal program termination(程序异常终止),然后终止程序。它还返回一个随实现而异的值,告诉操作系统(如果程序是由另一个程序调用的,则告诉父进程),处理失败。abort()是否刷新文件缓冲区(用于存储读写到文件中的数据的内存区域)取决于实现。如果愿意,也可以使用exit(),该函数刷新文件缓冲区,但不显示消息。
和上面的assert类似,可用于空指针检查时以及检查函数参数的值,这个需要用到if语句,例如:
if(pointer != NULL){
std::abort();
}
throw-catch异常机制
异常提供了将控制权从程序的一个部分传递到另一个部分的途径,对异常的处理有3个组成部分:
- 引发异常;
- 使用处理程序捕捉异常;
- 使用try块。
程序在出现问题时将引发异常。例如,可以修改前面的if语句,使之引发异常,而不是调用abort()函数。throw语句实际上是跳转,即命令程序跳转到另一条语句。throw关键字表示引发异常,紧随其后的值(例如字符串或对象)指出了异常的特征。
程序使用异常处理程序(exception handler)来捕捉异常,异常处理程序位于要处理的程序中。catch关键字表示捕捉异常。处理程序以关键字catch开头,随后是位于括号中的类型声明,它指出了异常处理程序要响应的异常类型;然后是一个花括号括起的代码块,指出要采取的措施。catch关键字和异常类型用作标签,指出当异常被引发时,程序应跳到这个位置执行。异常处理程序也被称为catch块。
try块表示其中特定的异常可能被激活的代码块,他后面跟一个或多个catch块。try块是由关键字try指示的,关键字try的后面是一个花括号括起的代码块,表明需要注意这些代码引发的异常。
出现异常时的程序流程如下图所示:
执行throw语句类似于执行返回语句,因为它也将终止函数的执行;但throw不是将控制权返回给调用程序,而是导致程序沿函数调用序列后退,直到找到包含try块的函数。
catch块点类似于函数定义,但并不是函数定义。关键字catch表明这是一个处理程序,当异常与该处理程序匹配时,程序将执行括号中的代码。
执行完try块中的语句后,如果没有引发任何异常,则程序跳过try块后面的catch块,直接执行处理程序后面的第一条语句。