标题:C++标准库的学习笔记 -- 诊断
只看楼主
pangding
Rank: 19Rank: 19Rank: 19Rank: 19Rank: 19Rank: 19
来 自:北京
等 级:贵宾
威 望:94
帖 子:6784
专家分:16751
注 册:2008-12-20
结帖率:96%
 问题点数:0 回复次数:5 
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 对意外终止的程序执行核心转储时输出的,与我展示的程序无关。不同的编译器输出的内容很可能不同,但应该没有什么本质区别。
    如果你用的是 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)这条信息。

    加 -D 选项定义 NDEBUG 之后,会忽略所有断言。用以下命令行编译,并执行:
$ g++ -DNDEBUG assertion.cpp
$ ./a.out
-nan
$
    你也可以在源代码的第一行加上一句 #define NDEBUG,效果一样。不过不改动源代码就可以编译生效是很有用的,试想如果你的源码分散在数个文件里,那么需要时,在每个文件开始加上一行,不需要时再删去是非常费时费力的,而且还容易有所遗漏。在 Unix 下的经典解决办法,一般是在 CFLAGS,或者 CXXFLAGS 里加上 -DNDEBUG 就行了。相信这么常用的功能 visual studio 之类的编译器里应该也有相应的设置方法,知道的朋友可以补充上来。

错误码:
    错误码也是 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 .

    我在上面的那个程序里尝试打开一个根本不存在的文件,因此肯定会返回 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 编辑 ]
搜索更多相关主题的帖子: 诊断 学习 设计程序 number 
2012-04-20 01:06
pangding
Rank: 19Rank: 19Rank: 19Rank: 19Rank: 19Rank: 19
来 自:北京
等 级:贵宾
威 望:94
帖 子:6784
专家分:16751
注 册:2008-12-20
得分:0 
先占几楼。
2012-04-20 01:08
pangding
Rank: 19Rank: 19Rank: 19Rank: 19Rank: 19Rank: 19
来 自:北京
等 级:贵宾
威 望:94
帖 子:6784
专家分:16751
注 册:2008-12-20
得分:0 
关于诊断的内容不是很丰富,而且很多问题也没展开讲。这回一次性就写完了,感觉还不错。
2012-04-20 01:10
hellovfp
Rank: 16Rank: 16Rank: 16Rank: 16
等 级:禁止访问
威 望:30
帖 子:2976
专家分:7697
注 册:2009-7-21
得分:0 
呵呵,内容很丰满,支持P版。

我们都在路上。。。。。
2012-04-21 11:25
lintaoyn
Rank: 11Rank: 11Rank: 11Rank: 11
等 级:小飞侠
威 望:4
帖 子:605
专家分:2489
注 册:2009-4-8
得分:0 
断言和异常分别用在什么情况,没讲清楚。windows下的几个断言函数和结构化异常转化成C++异常也可以当成扩展内容来讲下

迭代的是人,递归的是神。
2012-04-24 07:35
pangding
Rank: 19Rank: 19Rank: 19Rank: 19Rank: 19Rank: 19
来 自:北京
等 级:贵宾
威 望:94
帖 子:6784
专家分:16751
注 册:2008-12-20
得分:0 
回复 5楼 lintaoyn
我以为:断言主要是用在开发阶段用来捕获逻辑错误,发布的时候全部都要去掉。g++ 的头文件里也能看到好多断言。其它可能的执行错误能可以用异常,异常应该是 c++ 里首选的错误处理方法吧。

我更多的是用 C。就我个人来讲,更喜欢 C 的哲学而不是 C++ 的,我也很少用 C++ 写程序。异常我就几乎没用过,所以讲得很多东西层次都比较浅。换句话说,我只对 C++ 的实现比较感兴趣,而不是如何用 C++ 搞开发。(可惜我最近写的这几篇文章也没有怎么介绍相关实现的细节。除了因为我本人在这方面的水平很有限以外,也因为这实在不是一两句话能解释的,除了得上大量的代码,还需要组织很多资料。)大家如果有相关的开发经验的可以补充进来。我这个帖子主要就是讲讲语法。
另外不同的语言环境也许会对 C++ 标准库做扩展。在 linux 下,最首选的编程语言可能是 sh-script。不过当它不胜任时,一般如果不用 C,普遍的选择可能是 python 或者 java。跨平台对于 Unix 家族来说很重要。windows 的那套东西我不懂,lintaoyn 如果能简要的做些介绍,我很欢迎。
2012-04-24 23:57



参与讨论请移步原网站贴子:https://bbs.bccn.net/thread-366209-1-1.html




关于我们 | 广告合作 | 编程中国 | 清除Cookies | TOP | 手机版

编程中国 版权所有,并保留所有权利。
Powered by Discuz, Processed in 0.022140 second(s), 7 queries.
Copyright©2004-2024, BCCN.NET, All Rights Reserved