<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 = %d\n", 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:\\CDemo\main.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 = %d\n", 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 宏后,导致程序在 Debug 模式和 Release 模式下的运行结果大相径庭的罪魁祸首就是assert(++i <= 100)
会被替换为((void)0)
。
++
运算,我们本来希望它改变 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 的终止条件时,结束循环。