9.1 assert宏的使用及注意事项

在讲解assert宏之前,先对断言进行基本介绍,让读者对断言有一个大致了解。在使用C语言编程时,我们总会对某种假设条件进行检查,断言就用于在代码中捕捉这些假设,可以将断言看做异常处理的一种高级形式。断言表示为一些布尔表达式,程序员相信在程序中的某个特定点处的表达式值为真。可以在任何时候启用和禁用断言验证,比如可以在测试时启用断言,而在部署时禁用断言。同样,在程序投入运行后,最终用户在遇到问题时可以重新启用断言。通过断言可以快速发现并定位软件的问题,同时对系统错误进行自动报警。对于在系统中隐藏很深,用其他手段极难发现的问题可以用断言来定位,从而缩短问题定位时间,提高系统的可测性。

接下来介绍C语言系统所提供的用于实现断言的宏assert,其一般形式为:


assert(expression);


参数expression可以是一般的常量、表达式、函数等。在使用assert宏的时候,先计算expression,如果expression的值为假(即为0),那么它先向stderr打印一条出错信息,然后通过调用abort()函数来终止程序运行。看看下面的代码。


include<stdio.h>

include<assert.h>

int main(void)

{

int i;

i=1;

assert(i++);

printf("通过assert宏进行i++运算之后的i值为:%d\n",i);

return 0;

}


运行结果:


通过assert宏进行i++运算之后的i值为:2


分析运行结果,因为给定的i初始值为1,所以使用“assert(i++);”语句时不会出现错误,进而执行了i++,其后的打印语句输出值为2。如果把i的初始值改为0,那么就会出现如下错误。


Assertion failed:i++,file E:\fdsa\fdsag.cpp,line 8


根据提示,是不是很快就能定位出错点呢?既然assert这么便于定位出错点,看来的确有必要在代码中熟练使用它,但是任何东西的使用都是有规则的,assert也不例外。

断言语句不需要总被执行,可以屏蔽,也可以启用,这就要求assert不管是在屏蔽状态还是启用状态都不能对代码本身的功能有所影响,因此之前在代码中使用了一句“assert(i++);”是不妥的,一旦禁用了assert,i++的语句就得不到执行,接下来对i值的使用就会出现问题。对于这样的语句应该分开实现,可以用如下两句来替代。


assert(i);

i++;


这就对断言的使用有了相应的要求,那么,一般在什么情况下使用断言呢?

可以在正常情况下程序不会到达的地方放置断言。

使用断言测试方法执行的前置条件和后置条件。

使用断言检查程序中变量的状态,确保变量的状态必须满足。

对于上面提到的前置条件和后置条件,有的读者可能还不是很了解,现解释如下:

前置条件断言:代码执行之前必须具备的特性。

后置条件断言:代码执行之后必须具备的特性。

前后不变断言:代码执行前后不能变化的特性。

当然在使用断言的过程中会有一些应该注意的事项和需要养成的一些良好习惯,如:

每个assert只检验一个条件,如果同时检验多个条件且结果断言失败,那就无法直观地判断哪个条件导致失败。

不能使用改变环境的语句,比如上面的代码改变了i变量,在实际编写代码的过程中不能这样做。

assert和后面的语句之间应空一行,以形成逻辑和视觉上的一致感,使编写的代码有一种视觉上的美感。

有时,assert不能代替条件过滤。

放在函数参数的入口处检查传入参数的合法性。

断言语句不可以有任何边界效应。

上面那么多文字,似乎很枯燥,但是要先坚持看完文字描述部分,才能在分析代码的过程中很快知道为什么会出现那样的问题,也才能在自己编写代码的时候熟练使用assert,为代码调试带来极大的便利。尤其是在利用C语言开发工程项目的时候,如果能够在代码中合理地使用assert,那么创建出的代码会更稳定,质量更好且不易于出错。但凡优秀的程序员都能够很好地使用assert编写出高质量的代码来。

介绍了assert这么多的优点,当然也要说说它的缺点。使用assert的缺点是,频繁调用会极大地影响程序的性能,增加额外开销。因此在调试结束后,可以通过在包含“#include”的语句之前插入#define NDEBUG来禁用assert调用。接下来看下面的一段代码。


include<stdio.h>

//#define NDEBUG

include<assert.h>

int copy_string(char from[],char to[])

{

int i=0;

while(to[i++]=from[i]);

return 1;

}

int main()

{

int ret;

char str[]="this is a string!";

char dec_str[50];

printf("main函数中的str字符串为:%s\n",str);

ret=copy_string(str,dec_str);

assert(ret);

printf("复杂成功后的dec_str字符串为:%s\n",dec_str);

return 0;

}


运行结果:


main函数中的str字符串为:this is a string!

复杂成功后的dec_str字符串为:this is a string!


在以上代码的开头部分,“#define NDEBUG”被注释掉了,所以启用了assert,main函数中使用的“assert(ret)”,而不是“assert(copy_string(str,dec_str))”,这样就避免了由于关闭测试而带来的函数copy_string(str,dec_str)不会被执行的情况。在调试的时候,只需要检测函数的返回值是否满足条件就可以了,而不用将函数作为assert的参数。因此在调试通过的情况下,关闭断言后不会对代码造成任何的影响,最终成功打印了两条输出语句。

需要注意的是,关闭断言的时候,“#define NDEBUG”必须写在“#include<assert.h>”的前面,否则将不能关闭断言。看看下面的代码。


include<stdio.h>

include<assert.h>

define NDEBUG

int main()

{

assert(0);

printf("成功关闭断言\n");

return 0;

}


在上面的代码中,按照前面的方式关闭断言后运行结果是“Assertion failed:0,file E:\fdsa\fdsag.cpp,line 7”,并且程序运行失败,这是因为并没有成功关闭断言,程序还是按照启用assert的方式来进行编译,同时调用abort()函数终止了程序的运行。所以在关闭断言的时候,需要将“#define NDEBUG”放在加载assert.h头文件语句的前面。