<assert.h> -- 宏函数
阅读:0       作者:严长生

assert() 断言函数,用于在调试过程中捕捉程序错误

void assert (int expression);

断言函数,用于在调试过程中捕捉程序的错误。

“断言”在语文中的意思是“断定”、“十分肯定地说”,在编程中是指对某种假设条件进行检测,如果条件成立就不进行任何操作,如果条件不成立就捕捉到这种错误,并打印出错误信息,终止程序执行。

assert() 会对表达式expression进行检测:
  • 如果expression的结果为 0(条件不成立),那么断言失败,表明程序出错,assert() 会向标准输出设备(一般是显示器)打印一条错误信息,并调用 abort() 函数终止程序的执行。
  • 如果expression的结果为非 0(条件成立),那么断言成功,表明程序正确,assert() 不进行任何操作。

要打印的错误信息的格式和内容没有统一的规定,这和标准库的具体实现有关(也可以说和编译器有关),但是错误信息至少应该包含以下几个方面的信息:
  • 断言失败的表达式,也即expression
  • 源文件名称;
  • 断言失败的代码的行号。

大部分编译器的格式如下所示:

Assertion failed: expression, file filename, line number

参数

  • expression

    要检测的表达式。如果表达式的值为 0,那么断言失败,程序终止执行;如果表达式的值为非 0,那么断言成功,assert() 不进行任何操作。

返回值

无返回值

assert() 的用法和机制

在大部分编译器下,assert() 是一个宏;在少数的编译器下,assert() 就是一个函数。我们无需关心这些差异,只管把 assert() 当做函数使用即可。

assert() 的用法很简单,我们只要传入一个表达式,它会计算这个表达式的结果:如果表达式的结果为“假”,assert() 会打印出断言失败的信息,并调用 abort() 函数终止程序的执行;如果表达式的结果为“真”,assert() 就什么也不做,程序继续往后执行。

下面是一个具体的例子:
#include <stdio.h>
#include <assert.h>
int main(){
    int m, n, result;
    scanf("%d %d", &m, &n);
    assert(n != 0);  //写作 assert(n) 更加简洁
    result = m / n;
    printf("result = %dn", result);
    return 0;
}
本例用来计算两个数相除的结果,由于被除数不能为 0,所以我们加入了 assert() 来检测错误。

如果输入100 20,那么 n 的值为 20,n != 0这个条件成立,assert() 不进行任何操作,最终的输出结果为:

100 20↙
result = 5

如果输入100 0,那么 n 的值为 0,n != 0这个条件不成立,assert() 就会报告错误,并终止程序执行,最终的结果如下所示:

100 0↙
Assertion failed: n != 0,  file E:CDemomain.c, line 6

NDEBUG 宏

如果查看 <assert.h> 头文件的源码,会发现 assert() 被定义为下面的样子:
#ifdef NDEBUG
#define assert(e) ((void)0)
#else
#define assert(e)  
    ((void) ((e) ? ((void)0) : __assert (#e, __FILE__, __LINE__)))
#endif
这意味着,一旦定义了NDEBUG宏,assert() 就无效了。

NDEBUG 是”No Debug“的意思,也即“非调试”。有的编译器(例如 Visual Studio)在发布(Release)模式下会定义 NDEBUG 宏,在调试(Debug)模式下不会定义定义这个宏;有的编译器(例如 Xcode)在发布模式和调试模式下都不会定义 NDEBUG 宏,这样当我们以发布模式编译程序时,就必须自己在编译参数中增加 NDEBUG 宏,或者在包含 <assert.h> 头文件之前定义 NDEBUG 宏。

调试模式是程序员在测试代码期间使用的编译模式,发布模式是将程序提供给用户时使用的编译模式。在发布模式下,我们不应该再依赖 assert() 宏,因为程序一旦出错,assert() 会抛出一段用户看不懂的提示信息,并毫无预警地终止程序执行,这样会严重影响软件的用户体验,所以在发布模式下应该让 assert() 失效。

修改上面的代码,在包含 <assert.h> 之前定义 NDEBUG 宏:
#define NDEBUG

#include <stdio.h>
#include <assert.h>

int main(){
    int m, n, result;
    scanf("%d %d", &m, &n);
    assert(n);
    result = m / n;
    printf("result = %dn", result);
    return 0;
}
当以发布模式编译这段代码时,assert() 就会失效。如果希望继续以调试模式编译这段代码,去掉 NDEBUG 宏即可。
在代码中显式地增加 NDEBUG 宏比较麻烦,因为当以调试模式编译代码时还得再去掉它,更加科学的做法是在 IDE 的编译参数中添加 NDEBUG 宏。不同的 IDE 添加宏的方式不同,这里不再深入探讨。

注意事项

1) 使用 assert() 时,被检测的表达式最好不要太复杂,以下面的代码为例:

assert( expression1 && expression2 && expression3);

当发生错误时,assert() 只会告诉我们expression1 && expression2 && expression3整个表达式为不成立,但是这个大的表达式还包含了三个小的表达式,并且它们之间是&&运算,任何一个小表达式为不成立都会导致整个表达式为不成立,这样我们就无法推断到底是expression1有问题,还是expression2或者expression3有问题,从而给排错带来麻烦。

这里我们应该遵循使用 assert() 的一个原则:每次断言只能检验一个表达式。根据这个原则,上面的代码应改为:

assert(expression1);
assert(expression2);
assert(expression3);

如此,一旦程序出错,我们就知道是哪个小的表达式断言失败了,从而快速定位到有问题的代码。

2) 使用 assert() 的另外一个注意事项是:不要用会改变环境的语句作为断言的表达式。请看下面的代码:
#include <stdio.h>
#include <assert.h>

int main(){
    int i = 0;
    while(i <= 110){
        assert(++i <= 100);
        printf("我是第%d行n",i);
    }
    return 0;
}
在 Debug 模式下运行,程序循环到第 101 次时,i 的值为 100,++i <= 100不再成立,断言失败,程序终止运行。

在 Release 模式下运行,编译参数中设置了 NDEBUG 宏(如果编译器没有默认设置,那么需要你自己来设置),assert() 会失效,++i <= 100这个表达式也不起作用了,while() 无法终止,成为一个死循环。
定义了 NDEBUG 宏后,assert(++i <= 100)会被替换为((void)0)
导致程序在 Debug 模式和 Release 模式下的运行结果大相径庭的罪魁祸首就是++运算,我们本来希望它改变 i 的值,逐渐达到 while 的终止条件,但是在 Release 模式下它失效了。

修改这段不规范的代码也很容易,将++i移出 assert() 即可:
#include <stdio.h>
#include <assert.h>

int main(){
    int i = 0;
    while(i <= 110){
        ++i;
        assert(i <= 100);
        printf("我是第%d行n",i);
    }
    return 0;
}
如此,不管是 Debug 模式还是 Release 模式,每次循环 i 的值都会增加,逐渐达到 while 的终止条件时,结束循环。