异常机制提供程序中错误检测与错误处理部分之间的通信。C++ 的异常处理中包括:
系统通过 throw 表达式抛出异常。throw 表达式由关键字 throw 以及尾随的表达式组成,通常以分号结束,这样它就成为了表达式语句。throw 表达式的类型决定了所抛出异常的类型。
一个简单的例子:该程序检查读入的记录是否来自同一本书。如果不是,就用 throw 抛出异常:
1 | // first check that data is for the same item |
这段代码检查 ISBN 对象是否不相同。如果不同的话,停止程序的执行,并将控制转移给处理这种错误的处理代码。 throw 语句使用了一个表达式。在本例中,该表达式是 runtime_error
类型的对象。runtime_error
类型是标准库异常类中的一种,在 stdexcept 头文件中定义。我们通过传递string 对象来创建 runtime_error
对象,这样就可以提供更多关于所出现问题的相关信息。
try 块的通用语法形式是:
1 | try |
try 块以关键字 try 开始,后面是用花括号起来的语句序列块。try 块后面是一个或多个 catch 子句。每个 catch 子句包括三部分:关键字 catch,圆括号内单个类型或者单个对象的声明——称为异常说明符,以及通常用花括号括起来的语句块。如果选择了一个 catch 子句来处理异常,则执行相关的块语句。一旦 catch 子句执行结束,程序流程立即继续执行紧随着最后一个 catch 子句的语句。
try 语句内的 program-statements 形成程序的正常逻辑。这里面可以包含任意 C++ 语句,包括变量声明。与其他块语句一样,try 块引入局部作用域,在 try 块中声明的变量,包括 catch 子句声明的变量,不能在 try 外面引用。
在前面的例子中,使用了 throw 来避免将两个表示不同书的 Sales_items
对象相加。想象一下将 Sales_items
对象相加的那部分程序与负责与用户交流的那部分是分开的,则与用户交互的部分也许会包含下面的用于处理所捕获异常的代码:
1 | while (cin >> item1 >> item2) { |
关键字 try 后面是一个块语句。这个块语句调用处理 Sales_item
对象的程序部分。这部分也可能会抛出 runtime_error
类型的异常。
上述 try 块提供单个 catch 子句,用来处理 runtime_error
类型的异常。在执行 try 块代码的过程中,如果在 try 块中的代码抛出 runtime_error
类型的异常,则处理这类异常的动作在 catch 后面的块语句中定义。
通过输出 err.what()
的返回值提示用户。大家都知道 err 返回 runtime_error
类型的值,因此可以推断出 what 是 runtime_error
类的一个成员函数。 每一个标准库异常类都定义了名为 what 的成员函数。这个函数不需要参数,返回 C 风格字符串。在出现 runtime_error
的情况下,what 返回的 C 风格字符串,是用于初始化 runtime_error
的 string 对象的副本。如果在前面章节描述的代码抛出异常,那么执行这个 catch 将输出。
C++ 标准库定义了一组类,用于报告在标准库中的函数遇到的问题。程序员可在自己编写的程序中使用这些标准异常类。标准库异常类定义在四个头文件中:
exception
最常见的问题。
runtime_error
运行时错误:仅在运行时才能检测到问题
range_error
运行时错误:生成的结果超出了有意义的值域范围
overflow_error
运行时错误:计算上溢
underflow_error
运行时错误:计算下溢
logic_error
逻辑错误:可在运行前检测到问题
domain_error
逻辑错误:参数的结果值不存在
invalid_argument
逻辑错误:不合适的参数
length_error
逻辑错误:试图生成一个超出该类型最大长度的对象
out_of_range
逻辑错误:使用一个超出有效范围的值
bad_alloc
异常类型,提供因无法分配内存而由 new 抛出的异常。bad_cast
异常类型。标准库异常类只提供很少的操作,包括创建、复制异常类型对象以及异常类型对象的赋值。 exception、bad_alloc 以及 bad_cast 类型只定义了默认构造函数,无法在创建这些类型的对象时为它们提供初值。其他的异常类型则只定义了一个使用 string 初始化式的构造函数。当需要定义这些异常类型的对象时,必须提供一想 string 参数。string 初始化式用于为所发生的错误提供更多的信息。
异常类型只定义了一个名为 what
的操作。这个函数不需要任何参数,并且返回
const char* 类型值。它返回的指针指向一个 C 风格字符串(第 4.3
节)。使用 C 风格字符串的目的是为所抛出的异常提出更详细的文字描述。
what 函数所返回的指针指向 C 风格字符数组的内容,这个数组的内容依赖于异常对象的类型。对于接受 string 初始化式的异常类型,what 函数将返回该 string 作为 C 风格字符数组。对于其他异常类型,返回的值则根据编译器的变化而不同。
可使用 NDEBUG 预处理变量实现有条件的调试代码:
1 | int main() |
如果 NDEBUG 未定义,那么程序就会将信息写到 cerr 中。如果 NDEBUG 已经定义了,那么程序执行时将会跳过 #ifndef
和 #endif
之间的代码。
默认情况下,NDEBUG 未定义(表示正在Debug!),这也就意味着必须执行 #ifndef
和 #endif
之间的代码。在开发程序的过程中,只要保持 NDEBUG
未定义就会执行其中的调试语句。开发完成后,要将程序交付给客户时,可通过定义 NDEBUG 预处理变量,(有效地)删除这些调试语句。
大多数的编译器都提供定义 NDEBUG 命令行选项 :
1 | $ CC -DNDEBUG main.c |
这样的命令行等效于在 main.c 的开头提供 #define NDEBUG
预处理命令。
预处理器还定义了其余四种在调试时非常有用的常量:
__FILE__
文件名
__LINE__
当前行号
__TIME__
文件被编译的时间
__DATE__
文件被编译的日期
可使用这些常量在错误消息中提供更多的信息:
1 | if (word.size() < threshold) |
如果给这个程序提供一个比 threshold 短的 string 对象,则会产生下面的错误信息:
1 | Error: wdebug.cc : line 21 |
另一个常见的调试技术是使用 NDEBUG 预处理变量以及 assert
预处理宏。assert 宏是在 cassert
头文件中定义的,所有使用 assert 的文件都必须包含这个头文件。
预处理宏有点像函数调用。assert 宏需要一个表达式作为它的条件:
1 | assert(expr) |
与异常不同(异常用于处理程序执行时预期要发生的错误),程序员使用 assert 来 测试“不可能发生”的条件 。例如,对于处理输入文本的程序,可以预测全部给出的单词都比指定的阈值长。那么程序可以包含这样一个语句:
1 | assert(word.size() > threshold); |
在测试过程中,assert 等效于检验数据是否总是具有预期的大小。一旦开发和测试工作完成,程序就已经建立好,并且定义了 NDEBUG。 在成品代码中,assert 语句不做任何工作,因此也没有任何运行时代价 。当然,也不会引起任何运行时检查。assert 仅用于检查确实不可能的条件,这只对程序的调试有帮助,但不能用来代替运行时的逻辑检查,也不能代替对程序可能产生的错误的检测。