断言assert函数,C语言assert函数完全攻略
对于断言,相信大家都不陌生,大多数编程语言也都有断言这一特性。简单地讲,断言就是对某种假设条件进行检查。在 C 语言中,断言被定义为宏的形式(assert(expression)),而不是函数,其原型定义在
文件中。其中,assert 将通过检查表达式 expression 的值来决定是否需要终止执行程序。也就是说,如果表达式 expression 的值为假(即为 0),那么它将首先向标准错误流 stderr 打印一条出错信息,然后再通过调用 abort 函数终止程序运行;否则,assert 无任何作用。
默认情况下,assert 宏只有在 Debug 版本(内部调试版本)中才能够起作用,而在 Release 版本(发行版本)中将被忽略。当然,也可以通过定义宏或设置编译器参数等形式来在任何时候启用或者禁用断言检查(不建议这么做)。同样,在程序投入运行后,最终用户在遇到问题时也可以重新起用断言。这样可以快速发现并定位软件问题,同时对系统错误进行自动报警。对于在系统中隐藏很深,用其他手段极难发现的问题也可以通过断言进行定位,从而缩短软件问题定位时间,提高系统的可测性。
尽量利用断言来提高代码的可测试性
在讨论如何使用断言之前,先来看下面一段示例代码:
- void *Memcpy(void *dest, const void *src, size_t len)
- {
- char *tmp_dest = (char *)dest;
- char *tmp_src = (char *)src;
- while(len –)
- *tmp_dest ++ = *tmp_src ++;
- return dest;
- }
- void *Memcpy(void *dest, const void *src, size_t len)
- {
- if(dest == NULL)
- {
- fprintf(stderr,”dest is NULL\n”);
- abort();
- }
- if(src == NULL)
- {
- fprintf(stderr,”src is NULL\n”);
- abort();
- }
- char *tmp_dest = (char *)dest;
- char *tmp_src = (char *)src;
- while(len –)
- *tmp_dest ++ = *tmp_src ++;
- return dest;
- }
- void *MemCopy(void *dest, const void *src, size_t len)
- {
- #ifdef DEBUG
- if(dest == NULL)
- {
- fprintf(stderr,”dest is NULL\n”);
- abort();
- }
- if(src == NULL)
- {
- fprintf(stderr,”src is NULL\n”);
- abort();
- }
- #endif
- char *tmp_dest = (char *)dest;
- char *tmp_src = (char *)src;
- while(len –)
- *tmp_dest ++ = *tmp_src ++;
- return dest;
- }
- void *MemCopy(void *dest, const void *src, size_t len)
- {
- assert(dest != NULL && src !=NULL);
- char *tmp_dest = (char *)dest;
- char *tmp_src = (char *)src;
- while(len –)
- *tmp_dest ++ = *tmp_src ++;
- return dest;
- }
- /*使用断言测试*/
- #ifdef DEBUG
- /*处理函数原型*/
- void Assert(char * filename, unsigned int lineno);
- #define ASSERT(condition)\
- if(condition)\
- NULL; \
- else\
- Assert(__FILE__ , __LINE__)
- /*不使用断言测试*/
- #else
- #define ASSERT(condition) NULL
- #endif
- void Assert(char * filename, unsigned int lineno)
- {
- fflush(stdout);
- fprintf(stderr,”\nAssert failed: %s, line %u\n”,filename, lineno);
- fflush(stderr);
- abort();
- }
- #define ASSERT(condition)\
- do{ \
- if(condition)\
- NULL; \
- else\
- Assert(__FILE__ , __LINE__);\
- }while(0)
- 现在,将不再为分号“;”而担心了,调用示例如下:
- void Test(unsigned char *str)
- {
- ASSERT(str != NULL);
- /*函数处理代码*/
- }
- int main(void)
- {
- Test(NULL);
- return 0;
- }
很显然,因为调用语句“Test(NULL)”为参数 str 错误传入一个 NULL 指针的原因,所以 ASSERT 宏会自动检测到这个错误,同时根据宏 __FILE__ 和 __LINE__ 所提供的文件名和行号参数在标准错误输出设备 stderr 上打印一条错误消息,然后调用 abort 函数中止程序的执行。运行结果如图 1 所示。
- void Test(unsigned char *str)
- {
- assert(str != NULL);
- /*函数处理代码*/
- }
毋庸置疑,标准 assert 宏同样会自动检测到这个 NULL 指针错误。与此同时,标准 assert 宏除给出以上信息之外,还能够显示出已经失败的测试条件。运行结果如图 2 所示。
尽量在函数中使用断言来检查参数的合法性
在函数中使用断言来检查参数的合法性是断言最主要的应用场景之一,它主要体现在如下 3 个方面:
- 在代码执行之前或者在函数的入口处,使用断言来检查参数的合法性,这称为前置条件断言。
- 在代码执行之后或者在函数的出口处,使用断言来检查参数是否被正确地执行,这称为后置条件断言。
- 在代码执行前后或者在函数的入出口处,使用断言来检查参数是否发生了变化,这称为前后不变断言。
- void *Memcpy(void *dest, const void *src, size_t len)
- {
- assert(dest!=NULL && src!=NULL);
- char *tmp_dest = (char *)dest;
- char *tmp_src = (char *)src;
- /*检查内存块是否重叠*/
- assert(tmp_dest>=tmp_src+len||tmp_src>=tmp_dest+len);
- while(len –)
- *tmp_dest ++ = *tmp_src ++;
- return dest;
- }
除此之外,建议每一个 assert 宏只检验一个条件,这样做的好处就是当断言失败时,便于程序排错。试想一下,如果在一个断言中同时检验多个条件,当断言失败时,我们将很难直观地判断哪个条件失败。因此,下面的断言代码应该更好一些,尽管这样显得有些多此一举:
- assert(dest!=NULL);
- assert(src!=NULL);
最后,建议 assert 宏后面的语句应该空一行,以形成逻辑和视觉上的一致感,让代码有一种视觉上的美感。同时为复杂的断言添加必要的注释,可澄清断言含义并减少不必要的误用。
避免在断言表达式中使用改变环境的语句
默认情况下,因为 assert 宏只有在 Debug 版本中才能起作用,而在 Release 版本中将被忽略。因此,在程序设计中应该避免在断言表达式中使用改变环境的语句。如下面的示例代码所示:
- int Test(int i)
- {
- assert(i++);
- return i;
- }
- int main(void)
- {
- int i=1;
- printf(“%d\n”,Test(i));
- return 0;
- }
- int Test(int i)
- {
- assert(i);
- i++;
- return i;
- }
现在,无论是 Debug 版本,还是 Release 版本的输出结果都将为 2。
避免使用断言去检查程序错误
- char * Strdup(const char * source)
- {
- assert(source != NULL);
- char * result=NULL;
- size_t len = strlen(source) +1;
- result = (char *)malloc(len);
- assert(result != NULL);
- strcpy(result, source);
- return result;
- }
- char * Strdup(const char * source)
- {
- assert(source != NULL);
- char * result=NULL;
- size_t len = strlen(source)+1;
- result = (char *)malloc(len);
- if (result != NULL)
- {
- strcpy(result, source);
- }
- return result;
- }
总之记住一句话:断言是用来检查非法情况的,而不是测试和处理错误的。因此,不要混淆非法情况与错误情况之间的区别,后者是必然存在且一定要处理的。
尽量在防错性程序设计中使用断言来进行错误报警
- 内存检查:如果在内存的某些块中存放了一些具有某种类型和范围的数据,则可对它们做经常性检查。
- 标志检查:如果系统的状态是由某些标志指示的,可对这些标志做单独检查。
- 反向检查:对于一些从一种代码翻译成另一种代码或从一种系统翻译成另一种系统的数据或变量值,可以采用反向检查,即利用反向翻译来检查原始值的翻译是否正确。
- 状态检查:对于某些具有多个操作状态的复杂系统,若用某些特定的存储值来表示这些状态,则可通过单独检查存储值来验证系统的操作状态。
- 连接检查:当使用链表结构时,可检查链表的连接情况。
- 时间检查:如果已知道完成某项计算所需的最长时间,则可用定时器来监视这个时间。
- 其他检查:程序设计人员可经常仔细地对所使用的数据结构、操作序列和定时以及程序的功能加以考虑,从中得到要进行哪些检查的启发。
- 来自外部设备的输入数据,包括范围、属性是否正确。
- 由其他程序所提供的数据是否正确。
- 数据库中的数据,包括数组、文件、结构、记录是否正确。
- 操作员的输入,包括输入的性质、顺序是否正确。
- 栈的深度是否正确。
- 数组界限是否正确。
- 表达式中是否出现零分母情况。
- 正在运行的程序版本是否是所期望的(包括最后系统重新组合的日期)。
- 通过其他程序或外部设备的输出数据是否正确。
- for(i=0;i
- {
- /*处理代码*/
- }
- for(i=0;i!=count;i++)
- {
- /*处理代码*/
- }
- for(i=0;i
- {
- /*处理代码*/
- }
- assert(i==count);
用断言保证没有定义的特性或功能不被使用
在日常软件设计中,如果原先规定的一部分功能尚未实现,则应该使用断言来保证这些没有被定义的特性或功能不被使用。例如,某通信模块在设计时,准备提供“无连接”和“连接”这两种业务。但当前的版本中仅实现了“无连接”业务,且在此版本的正式发行版中,用户(上层模块)不应产生“连接”业务的请求,那么在测试时可用断言来检查用户是否使用了“连接”业务。如下面的示例代码所示:
- /*无连接业务*/
- #define CONNECTIONLESS 0
- /*连接业务*/
- #define CONNECTION 1
- int MessageProcess(MESSAGE *msg)
- {
- assert(msg != NULL);
- unsigned char service;
- service = GetMessageService(msg);
- /*使用断言来检查用户是否使用了“连接”业务*/
- assert(service != CONNECTION);
- /*处理代码*/
- }
谨慎使用断言对程序开发环境中的假设进行检查
- /*int类型占用的内存空间是否为2*/
- assert(sizeof(int)== 2);
- /*long类型占用的内存空间是否为4*/
- assert(sizeof(long)==4);
- /*byte的宽度是否为8*/
- assert(CHAR_BIT==8);
发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/212174.html原文链接:https://javaforall.net
