C++标准库的学习笔记 -- 诊断
诊断库(diagnostics library )诊断,顾名思议就是分析处理各种错误。除了包括在 C 语言中就已经有了的断言(assertions)和设置错误码(error number)这些方法以外,C++ 还有异常处理的一套手段。相信这些大家都已熟知。
诊断库的结构要比语言支持简单得多,就是我刚才说的那三部分。
关于诊断的问题,主要是在设计程序的时候能善用它们。因此谈论这个主题的意义并不是库本身,而是如何使用它。这需要相关的开发经验,简单的语法演示可能说明什么问题。当然最低层次是熟悉相关的语法,我在下面会非常简要地介绍一下相关的内容。
断言:
这是C中就有的内容,当然在 C++ 里可以结合很多 C++ 的方法来使用。要想使用断言,需要包含<cassert>。这个头文件里至少需要实现一个宏 assert,gcc 还扩展了一个宏 assert_perror。
另外还有一个标准规定的宏 NDEBUG,用以影响 assert 的行为。当该宏存定义时,所有断言均被忽略。不过当然要在包含 <cassert> 之前定义,熟悉 gcc 朋友应该知道可以用 -D 选项来表达。
程序代码:
#include <iostream> #include <cassert> #include <errono> #include <cmath> using namespace std; double my_sqrt(double x) { assert (x >= 0.0); return sqrt(x); } int main() { cout << my_sqrt(-5.0) << endl; return 0; }断言可以用来捕捉逻辑错误。当程序执行到某一个点的时候,如果从逻辑上讲某些条件必然成立,就可以在这个点上断言这个条件。当断言失败时,它会向 stderr 输出断言失败的位置:包括源文件,行数,施行断言的函数和断言的内容等等,并立即终止程序。因此断言失败后,程序员就可以根据输出的内容排错。所有断言的内容都可以在发布时去除,因此非常方便。
正常编译并执行上面那个程序,输出是这样的:
a.out: assertion.cpp:9: double my_sqrt(double): Assertion `x >= 0.0' failed.
Aborted (core dumped)
其中 a.out 是我调用的可执行文件的名字;assertion.cpp 是源文件的名字,后面的 9 表示断言在第9行;后面是函数的名字和断言的内容。在下面一行的内容是 Linux 对意外终止的程序执行核心转储时输出的,与我展示的程序无关。不同的编译器输出的内容很可能不同,但应该没有什么本质区别。Aborted (core dumped)
如果你用的是 gcc,那可以把 assert 那行换成:
assert_perror (x < 0.0 ? EDOM : 0);那么输出会变成:
a.out: assertion.cpp:9: double my_sqrt(double): Unexpected error: Numerical argument out of domain.
Aborted (core dumped)
这样输出的内容会更有提示性,因为它还额外输出了:数值参数不在定义域内(Numerical argument out of domain)这条信息。Aborted (core dumped)
加 -D 选项定义 NDEBUG 之后,会忽略所有断言。用以下命令行编译,并执行:
$ g++ -DNDEBUG assertion.cpp
$ ./a.out
-nan
$
你也可以在源代码的第一行加上一句 #define NDEBUG,效果一样。不过不改动源代码就可以编译生效是很有用的,试想如果你的源码分散在数个文件里,那么需要时,在每个文件开始加上一行,不需要时再删去是非常费时费力的,而且还容易有所遗漏。在 Unix 下的经典解决办法,一般是在 CFLAGS,或者 CXXFLAGS 里加上 -DNDEBUG 就行了。相信这么常用的功能 visual studio 之类的编译器里应该也有相应的设置方法,知道的朋友可以补充上来。$ ./a.out
-nan
$
错误码:
错误码也是 C 语言里一种很常见的处理错误的方法。因为很多时候,我们无法通过一个函数的返回值判断它执行失败的原因。比如 getc 在执行成功时会返回它读到的字符,在读到文件结束符或者执行失败的时候会返回 EOF,但是返回EOF究竟是因为读到了文件结束符还是发生了错误呢?如果发生了错误到底是发生了什么错误呢?我们完全无法从返回值看出任何端倪。因此在使用 C 的库函数时,要学会使用 errno。有关 errno 的使用,有很多值得注意的地方。很多讲 C 语言的书,或者深入介绍操作系统的书,都会非常详细的介绍有关的内容。对这些内容的介绍不在我的文章范围内。我只举个简单例子,以展示在 C 语言中 errno 的用法:
程序代码:
#include <iostream> #include <cerrno> #include <cstdio> using namespace std; int main() { FILE *fp = fopen("no_such_file.txt", "r"); // 假设不存在一个叫 no_such_file.txt 的文件。 if (fp == NULL) { int err_stat = errno; // 要在调用任何库函数前备份错误码,以供之后检查。 perror("fopen"); if (err_stat == ENOENT) printf("trying to handle this error.\n"); } return 0; }输出:
fopen: No such file or directory
trying to handle this error .
trying to handle this error .
我在上面的那个程序里尝试打开一个根本不存在的文件,因此肯定会返回 NULL。假设我不知道它失败的原因,那么我就可以在之后的那个 if 里尝试几种可能,比如我在这里就只尝试了 ENOENT (是 Error: NO ENTry 的缩写),它表示文件或目录不存在。(Unix 术语 目录(directory) 就是指 Windows 里的文件夹(folder))
我在这里只是简单打印了一句话,表示这个条件是成立的。在现实中,很多时候不能因为一点小错误就让程序退出。比如,如果我要求用户指定一个要打开的文件,然后一读发现文件不存在。那么也许可以返回去要求用户检查一下是不是文件名拼错了。或者是寻问一下用户,不存在的文件是不是需要创建。当打开一个文件以写入的时候,也可以再要求用户确认是不是可以覆盖已存在的文件之类的。有些错误是可以处理的,就应该尝试处理一下,然后让程序继续向下执行。
当然也有的时候,遇到了不可恢复的错误。这样的错误一般称为致命错误。比如在计算数据的时候发现了数据的不一致,这时如果没有任何纠错手段以恢复一致性,那么只能选择放弃计算。这比继续往下瞎算,并得到毫无意义的结果要好得多。或者在申请内存的时候失败了,那么也无法继续完成后续的计算。这时程序都要被迫退出,一个好的实践是在退出前输出一些东西,以提示用户。或者如果可能的话转储一些核心数据,以便日后分析程序被迫终止的原因。
有库函数 perror(也许是 Print ERROR 的缩写),它会先输出你给的参数和一个冒号加空格,之后再根据当前的 errno 在 stderr 上输出一个适当的字符串,以指示出错的原因。它输出的字符串,你也可以通过 strerror(errnum) 得到。(在 <string.h> 或者 <cstring> 里)
很遗憾,有关 errno 相关的很多问题都是不可移植的。所有与指示 errno 有关的宏都以 E 开头,并且标识符的其余部分也只能是数字和大写字母。这些取值与操作系统的支持和函数库的实现有很大的关系。C++ 标准里只规定了两个宏:EDOM 和 ERANGE 分别用于指示算术运算的定义域错误和运算结果超限。我之前讲的开方的那个例子里,就用到了 EDOM。注意 C++ 标准没有规定 ENOENT(这个是在 POSIX.1 标准里规定的),所以你的系统也许会编译不了我写的那个程序。
Linux 下的 gcc,包含 errno.h 或者 <cerrno> 会转而包含 linux/errno.h 这个头文件,而这个头文件最终会被引导至 asm-generic/errno.h。Linux 至少提供了 133 个以上的错误码。有兴趣的朋友可以参考这些头文件以获得和实现有关的更多信息。另外也可参考手册页,以得到相关的概述。
$man errno
其它的平台的朋友,也许也可以用下面这个方法看到系统支持那些错误的种类:
程序代码:
#include <iostream> #include <cerrno> #include <cstring> using namespace std; int main() { for (int i = 1; i < 256; i++) cout << i << ":\t" << strerror(i) << endl; return 0; }
异常:
不是第一次提到异常了。异常是 C++ 里处理错误的首选方法,之前介绍的两种方法,都大量涉及 C 语言中的东西。那两种方法我之所以介绍的比较详细,是因为这边可能有只学过 C++ 而没学过 C 的朋友,所以稍微科普一点相关的知识。在 C++ 里有异常机制之后,前两种方法就用的很少了,所以即使讲 C++ 的书会提到,恐怕也是一带而过。断言机制还好,但错误码的机制则是完全根植于 C 的思想。当然如果你要接触比如操作系统这样底层的东西,就免不了要和它们打交道。但用 C++ 写上层代码,就应该只考虑异常处理。
在之前介绍语言支持的时候,讲动态内存管理的时候,我提到了 C++ 标准库用的标准分配错误这个异常:bad_alloc,它的定义在 <new> 里。
在头文件 <stdexcept> 里也定义了数个异常。首选,最重要的一个是标准异常: exception。标准库中所有的异常都是它的子类。exception 里有一个叫 what() 的虚函数,会返回一个 const char * 用于指示自己是什么异常。因而它的子类也都有这个函数。但不同的实现输出的内容可能会略有差别。
程序代码:
#include <iostream> #include <stdexcept> #include <new> using namespace std; int main() { exception e; bad_alloc b; cout << e.what() << endl; // 输出:std::exception cout << b.what() << endl; // 输出:std::bad_alloc return 0; }
stdexcept 里还定义另外两大类异常,当然也都是 exception 的子类。一个是 logic_error,另一个是 runtime_error。
logic_error 又派生了四个类:domain_error, invalid_argument, length_error 和 out_of_range。
runtime_error 也派生了三个类:range_error, overflow_error 和 underflow_error。
它们之间的派生关系用个图可能会表示的更清楚,不过我就不画了。另外 bad_alloc 和这两个类是平行的。
很多标准库函数都会抛出这些异常。如果将来我讲其它库函数的时候遇到,再详细的讲也许更好。除了标准库会抛这些异常以外,我们也可以自己使用这些异常:
程序代码:
#include <iostream> #include <stdexcept> #include <cmath> #include <string> using namespace std; double my_sqrt(double x) throw(domain_error) { if (x < 0.0) // 对负数开方的话,抛出定义域错误异常。 throw domain_error("my_sqrt: extracting square root of negative."); return sqrt(x); } int main() { try { // 接自己抛投的异常。 my_sqrt(-5.0); } catch (domain_error &e) { cout << e.what() << endl; } // 输出:my_sqrt: extracting square root of negative. try { // 接库函数抛投的异常。 string s("12345"); // 构造一个长度为 5 的字符串。 s.at(10); // 断言 10 这个位置。 } catch (out_of_range &e) { cout << e.what() << endl; } // 输出:basic_string::at return 0; }
[ 本帖最后由 pangding 于 2012-4-21 10:56 编辑 ]