异常机制提供程序中错误检测与错误处理部分之间的通信。C++ 的异常处理中包括:

  1. throw 表达式,错误检测部分使用这种表达式来说明遇到了不可处理的错误。可以说,throw 引发了异常条件。
  2. try 块,错误处理部分使用它来处理异常。try 语句块以 try 关键字开始,并以一个或多个 catch 子句结束。在 try 块中执行的代码所抛出(throw)的异常,通常会被其中一个 catch 子句处理。由于它们“处理”异常,catch 子句也称为处理代码。
  3. 由标准库定义的一组异常类,用来在 throw 和相应的 catch 之间传递有关的错误信息。

throw 表达式

系统通过 throw 表达式抛出异常。throw 表达式由关键字 throw 以及尾随的表达式组成,通常以分号结束,这样它就成为了表达式语句。throw 表达式的类型决定了所抛出异常的类型。

一个简单的例子:该程序检查读入的记录是否来自同一本书。如果不是,就用 throw 抛出异常:

1
2
3
4
5
// first check that data is for the same item 
if (!item1.same_isbn(item2))
throw runtime_error("Data must refer to same ISBN");
// ok, if we're still here the ISBNs are the same
std::cout << item1 + item2 << std::endl;

这段代码检查 ISBN 对象是否不相同。如果不同的话,停止程序的执行,并将控制转移给处理这种错误的处理代码。 throw 语句使用了一个表达式。在本例中,该表达式是 runtime_error 类型的对象。runtime_error 类型是标准库异常类中的一种,在 stdexcept 头文件中定义。我们通过传递string 对象来创建 runtime_error 对象,这样就可以提供更多关于所出现问题的相关信息。

try 块

try 块的通用语法形式是:

1
2
3
4
5
6
7
8
try
{
program-statements
} catch (exception-specifier) {
handler-statements
} catch (exception-specifier) {
handler-statements
} //...

try 块以关键字 try 开始,后面是用花括号起来的语句序列块。try 块后面是一个或多个 catch 子句。每个 catch 子句包括三部分:关键字 catch,圆括号内单个类型或者单个对象的声明——称为异常说明符,以及通常用花括号括起来的语句块。如果选择了一个 catch 子句来处理异常,则执行相关的块语句。一旦 catch 子句执行结束,程序流程立即继续执行紧随着最后一个 catch 子句的语句。

try 语句内的 program-statements 形成程序的正常逻辑。这里面可以包含任意 C++ 语句,包括变量声明。与其他块语句一样,try 块引入局部作用域,在 try 块中声明的变量,包括 catch 子句声明的变量,不能在 try 外面引用。

示例

在前面的例子中,使用了 throw 来避免将两个表示不同书的 Sales_items 对象相加。想象一下将 Sales_items 对象相加的那部分程序与负责与用户交流的那部分是分开的,则与用户交互的部分也许会包含下面的用于处理所捕获异常的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
while (cin >> item1 >> item2) { 
try {
// execute code that will add the two Sales_items
// if the addition fails, the code throws a runtime_error exception
} catch (runtime_error err) {
// remind the user that ISBN must match and prompt for another pair
cout << err.what()
<< "\nTry Again? Enter y or n" << endl;
char c;
cin >> c;
if (cin && c == 'n')
break; // break out of the while loop
}
}

关键字 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 头文件定义了最常见的异常类,它的类名是 exception。这个类只通知异常的产生,但不会提供更多的信息。
  • stdexcept 头文件定义了几种常见的异常类,这些类型在下表中列出。

exception 最常见的问题。 runtime_error 运行时错误:仅在运行时才能检测到问题 range_error 运行时错误:生成的结果超出了有意义的值域范围 overflow_error 运行时错误:计算上溢 underflow_error 运行时错误:计算下溢 logic_error 逻辑错误:可在运行前检测到问题 domain_error 逻辑错误:参数的结果值不存在 invalid_argument 逻辑错误:不合适的参数 length_error 逻辑错误:试图生成一个超出该类型最大长度的对象 out_of_range 逻辑错误:使用一个超出有效范围的值


  • new 头文件定义了 bad_alloc 异常类型,提供因无法分配内存而由 new 抛出的异常。
  • type_info 头文件定义了 bad_cast 异常类型。

标准库异常类

标准库异常类只提供很少的操作,包括创建、复制异常类型对象以及异常类型对象的赋值。 exception、bad_alloc 以及 bad_cast 类型只定义了默认构造函数,无法在创建这些类型的对象时为它们提供初值。其他的异常类型则只定义了一个使用 string 初始化式的构造函数。当需要定义这些异常类型的对象时,必须提供一想 string 参数。string 初始化式用于为所发生的错误提供更多的信息。

异常类型只定义了一个名为 what 的操作。这个函数不需要任何参数,并且返回 const char* 类型值。它返回的指针指向一个 C 风格字符串(第 4.3 节)。使用 C 风格字符串的目的是为所抛出的异常提出更详细的文字描述。

what 函数所返回的指针指向 C 风格字符数组的内容,这个数组的内容依赖于异常对象的类型。对于接受 string 初始化式的异常类型,what 函数将返回该 string 作为 C 风格字符数组。对于其他异常类型,返回的值则根据编译器的变化而不同。

使用预处理器进行调试

NDEBUG 预处理变量

可使用 NDEBUG 预处理变量实现有条件的调试代码:

1
2
3
4
5
6
int main() 
{
#ifndef NDEBUG
cerr << "starting main" << endl;
#endif
// ...

如果 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
2
3
4
5
6
7
if (word.size() < threshold) 
cerr << "Error: " << __FILE__
<< " : line " << __LINE__ << endl
<< " Compiled on " << __DATE__
<< " at " << __TIME__ << endl
<< " Word read was " << word
<< ": Length too short" << endl;

如果给这个程序提供一个比 threshold 短的 string 对象,则会产生下面的错误信息:

1
2
3
Error: wdebug.cc : line 21 
Compiled on Jan 12 2005 at 19:44:40
Word read was "foo": Length too short

assert 宏

另一个常见的调试技术是使用 NDEBUG 预处理变量以及 assert 预处理宏。assert 宏是在 cassert 头文件中定义的,所有使用 assert 的文件都必须包含这个头文件。

预处理宏有点像函数调用。assert 宏需要一个表达式作为它的条件:

1
assert(expr)

与异常不同(异常用于处理程序执行时预期要发生的错误),程序员使用 assert 来 测试“不可能发生”的条件 。例如,对于处理输入文本的程序,可以预测全部给出的单词都比指定的阈值长。那么程序可以包含这样一个语句:

1
assert(word.size() > threshold); 

在测试过程中,assert 等效于检验数据是否总是具有预期的大小。一旦开发和测试工作完成,程序就已经建立好,并且定义了 NDEBUG。 在成品代码中,assert 语句不做任何工作,因此也没有任何运行时代价 。当然,也不会引起任何运行时检查。assert 仅用于检查确实不可能的条件,这只对程序的调试有帮助,但不能用来代替运行时的逻辑检查,也不能代替对程序可能产生的错误的检测。

Comments