断言assert函数,C语言assert函数彻底攻略

http://c.biancheng.net/c/assert/程序员

对于断言,相信你们都不陌生,大多数编程语言也都有断言这一特性。简单地讲,断言就是对某种假设条件进行检查。在 C 语言中,断言被定义为宏的形式(assert(expression)),而不是函数,其原型定义在<assert.h>文件中。其中,assert 将经过检查表达式 expression 的值来决定是否须要终止执行程序。也就是说,若是表达式 expression 的值为假(即为 0),那么它将首先向标准错误流 stderr 打印一条出错信息,而后再经过调用 abort 函数终止程序运行;不然,assert 无任何做用。

默认状况下,assert 宏只有在 Debug 版本(内部调试版本)中才可以起做用,而在 Release 版本(发行版本)中将被忽略。固然,也能够经过定义宏或设置编译器参数等形式来在任什么时候候启用或者禁用断言检查(不建议这么作)。一样,在程序投入运行后,最终用户在遇到问题时也能够从新起用断言。这样能够快速发现并定位软件问题,同时对系统错误进行自动报警。对于在系统中隐藏很深,用其余手段极难发现的问题也能够经过断言进行定位,从而缩短软件问题定位时间,提升系统的可测性。算法

尽可能利用断言来提升代码的可测试性

在讨论如何使用断言以前,先来看下面一段示例代码:数据库

  1. void *Memcpy(void *dest, const void *src, size_t len)
  2. {
  3. char *tmp_dest = (char *)dest;
  4. char *tmp_src = (char *)src;
  5. while(len --)
  6. *tmp_dest ++ = *tmp_src ++;
  7. return dest;
  8. }

对于上面的 Memcpy 函数,毋庸置疑,它可以经过编译程序的检查成功编译。从表面上看,该函数并不存在其余任何问题,而且代码也很是干净。

但遗憾的是,在调用该函数时,若是不当心为 dest 与 src 参数错误地传入了 NULL 指针,那么问题就严重了。轻者在交付以前这个潜在的错误致使程序瘫痪,从而暴露出来。不然,若是将该程序打包发布出去,那么所形成的后果是没法估计的。

因而可知,不可以简单地认为“只要经过编译程序成功编译的就都是安全的程序”。固然,编译程序也很难检查出相似的潜在错误(如所传递的参数是否有效、潜在的算法错误等)。面对这类问题,通常首先想到的应该是使用最简单的if语句进行判断检查,以下面的示例代码所示:express

  1. void *Memcpy(void *dest, const void *src, size_t len)
  2. {
  3. if(dest == NULL)
  4. {
  5. fprintf(stderr,"dest is NULL\n");
  6. abort();
  7. }
  8. if(src == NULL)
  9. {
  10. fprintf(stderr,"src is NULL\n");
  11. abort();
  12. }
  13. char *tmp_dest = (char *)dest;
  14. char *tmp_src = (char *)src;
  15. while(len --)
  16. *tmp_dest ++ = *tmp_src ++;
  17. return dest;
  18. }

如今,经过“if(dest==NULL)与if(data-src==NULL)”判断语句,只要在调用该函数的时候为 dest 与 src 参数错误地传入了NULL指针,这个函数就会检查出来并作出相应的处理,即先向标准错误流 stderr 打印一条出错信息,而后再调用 abort 函数终止程序运行。

从表面看来,上面的解决方案应该堪称完美。可是,随着函数参数或须要检查的表达式不断增多,这种检查测试代码将占据整个函数的大部分(这一点从上面的 Memcpy 函数中就不难看出)。这样代码看起来很是不简洁,甚至能够说很“糟糕”,并且也下降了函数的执行效率。

面对上面的问题,或许能够利用 C 的预处理程序有条件地包含或不包含相应的检查部分进行解决,以下面的代码所示:编程

  1. void *MemCopy(void *dest, const void *src, size_t len)
  2. {
  3. #ifdef DEBUG
  4. if(dest == NULL)
  5. {
  6. fprintf(stderr,"dest is NULL\n");
  7. abort();
  8. }
  9. if(src == NULL)
  10. {
  11. fprintf(stderr,"src is NULL\n");
  12. abort();
  13. }
  14. #endif
  15. char *tmp_dest = (char *)dest;
  16. char *tmp_src = (char *)src;
  17. while(len --)
  18. *tmp_dest ++ = *tmp_src ++;
  19. return dest;
  20. }

这样,经过条件编译“#ifdef DEBUG”来同时维护同一程序的两个版本(内部调试版本与发行版本),即在程序编写过程当中,编译其内部调试版本,利用其提供的测试检查代码为程序自动查错。而在程序编完以后,再编译成发行版本。

上面的解决方案尽管经过条件编译“#ifdef DEBUG”能产生很好的结果,也彻底符合咱们的程序设计要求,可是仔细观察会发现,这样的测试检查代码显得并不那么友好,当一个函数里这种条件编译语句不少时,代码会显得有些浮肿,甚至有些糟糕。

所以,对于上面的这种状况,多数程序员都会选择将全部的调试代码隐藏在断言 assert 宏中。其实,assert 宏也只不过是使用条件编译“#ifdef”对部分代码进行替换,利用 assert 宏,将会使代码变得更加简洁,以下面的示例代码所示:数组

  1. void *MemCopy(void *dest, const void *src, size_t len)
  2. {
  3. assert(dest != NULL && src !=NULL);
  4. char *tmp_dest = (char *)dest;
  5. char *tmp_src = (char *)src;
  6. while(len --)
  7. *tmp_dest ++ = *tmp_src ++;
  8. return dest;
  9. }

如今,经过“assert(dest !=NULL&&src !=NULL)”语句既完成程序的测试检查功能(即只要在调用该函数的时候为 dest 与 src 参数错误传入 NULL 指针时都会引起 assert),与此同时,对 MemCopy 函数的代码量也进行了大幅度瘦身,不得不说这是一个一箭双鵰的好办法。

实际上,在编程中咱们常常会出于某种目的(如把 assert 宏定义成当发生错误时不是停止调用程序的执行,而是在发生错误的位置转入调试程序,又或者是容许用户选择让程序继续运行等)须要对 assert 宏进行从新定义。

但值得注意的是,无论断言宏最终是用什么样的方式进行定义,其所定义宏的主要目的都是要使用它来对传递给相应函数的参数进行确认检查。若是违背了这条宏定义原则,那么所定义的宏将会偏离方向,失去宏定义自己的意义。与此同时,为不影响标准 assert 宏的使用,最好使用其余的名字。例如,下面的示例代码就展现了用户如何重定义本身的宏 ASSERT:安全

  1. /*使用断言测试*/
  2. #ifdef DEBUG
  3. /*处理函数原型*/
  4. void Assert(char * filename, unsigned int lineno);
  5. #define ASSERT(condition)\
  6. if(condition)\
  7. NULL; \
  8. else\
  9. Assert(__FILE__ , __LINE__)
  10. /*不使用断言测试*/
  11. #else
  12. #define ASSERT(condition) NULL
  13. #endif
  14. void Assert(char * filename, unsigned int lineno)
  15. {
  16. fflush(stdout);
  17. fprintf(stderr,"\nAssert failed: %s, line %u\n",filename, lineno);
  18. fflush(stderr);
  19. abort();
  20. }

若是定义了 DEBUG,ASSERT 将被扩展为一个if语句,不然执行“#define ASSERT(condition) NULL”替换成 NULL。

这里须要注意的是,由于在编写 C 语言代码时,在每一个语句后面加一个分号“;”已经成为一种约定俗成的习惯,所以颇有可能会在“Assert(__FILE__,__LINE__)”调用语句以后习惯性地加上一个分号。实际上并不须要这个分号,由于用户在调用 ASSERT 宏时,已经给出了一个分号。面对这种问题,咱们可使用“do{}while(0)”结构进行处理,以下面的代码所示:数据结构

  1. #define ASSERT(condition)\
  2. do{ \
  3. if(condition)\
  4. NULL; \
  5. else\
  6. Assert(__FILE__ , __LINE__);\
  7. }while(0)
  8. 如今,将再也不为分号“;”而担忧了,调用示例以下:
  9. void Test(unsigned char *str)
  10. {
  11. ASSERT(str != NULL);
  12. /*函数处理代码*/
  13. }
  14. int main(void)
  15. {
  16. Test(NULL);
  17. return 0;
  18. }

很显然,由于调用语句“Test(NULL)”为参数 str 错误传入一个 NULL 指针的缘由,因此 ASSERT 宏会自动检测到这个错误,同时根据宏 __FILE__ 和 __LINE__ 所提供的文件名和行号参数在标准错误输出设备 stderr 上打印一条错误消息,而后调用 abort 函数停止程序的执行。运行结果如图 1 所示。编程语言



图 1 调用自定义 ASSERT 宏的运行结果


若是这时候将自定义 ASSERT 宏替换成标准 assert 宏结果会是怎样的呢?以下面的示例代码所示:函数

  1. void Test(unsigned char *str)
  2. {
  3. assert(str != NULL);
  4. /*函数处理代码*/
  5. }

毋庸置疑,标准 assert 宏一样会自动检测到这个 NULL 指针错误。与此同时,标准 assert 宏除给出以上信息以外,还可以显示出已经失败的测试条件。运行结果如图 2 所示。



图 2 调用标准 assert 宏的运行结果


从上面的示例中不难发现,对标准的 assert 宏来讲,自定义的 ASSERT 宏将具备更大的灵活性,能够根据本身的须要打印输出不一样的信息,同时也能够对不一样类型的错误或者警告信息使用不一样的断言,这也是在工程代码中常用的作法。固然,若是没有什么特殊需求,仍是建议使用标准 assert 宏。

尽可能在函数中使用断言来检查参数的合法性

在函数中使用断言来检查参数的合法性是断言最主要的应用场景之一,它主要体如今以下 3 个方面:

  1. 在代码执行以前或者在函数的入口处,使用断言来检查参数的合法性,这称为前置条件断言。
  2. 在代码执行以后或者在函数的出口处,使用断言来检查参数是否被正确地执行,这称为后置条件断言。
  3. 在代码执行先后或者在函数的入出口处,使用断言来检查参数是否发生了变化,这称为先后不变断言。


例如,在上面的 Memcpy 函数中,除了能够经过“assert(dest !=NULL&&src!=NULL);”语句在函数的入口处检查 dest 与 src 参数是否传入 NULL 指针以外,还能够经过“assert(tmp_dest>=tmp_src+len||tmp_src>=tmp_dest+len);”语句检查两个内存块是否发生重叠。以下面的示例代码所示:

  1. void *Memcpy(void *dest, const void *src, size_t len)
  2. {
  3. assert(dest!=NULL && src!=NULL);
  4. char *tmp_dest = (char *)dest;
  5. char *tmp_src = (char *)src;
  6. /*检查内存块是否重叠*/
  7. assert(tmp_dest>=tmp_src+len||tmp_src>=tmp_dest+len);
  8. while(len --)
  9. *tmp_dest ++ = *tmp_src ++;
  10. return dest;
  11. }

除此以外,建议每个 assert 宏只检验一个条件,这样作的好处就是当断言失败时,便于程序排错。试想一下,若是在一个断言中同时检验多个条件,当断言失败时,咱们将很难直观地判断哪一个条件失败。所以,下面的断言代码应该更好一些,尽管这样显得有些画蛇添足:

  1. assert(dest!=NULL);
  2. assert(src!=NULL);

最后,建议 assert 宏后面的语句应该空一行,以造成逻辑和视觉上的一致感,让代码有一种视觉上的美感。同时为复杂的断言添加必要的注释,可澄清断言含义并减小没必要要的误用。

避免在断言表达式中使用改变环境的语句

默认状况下,由于 assert 宏只有在 Debug 版本中才能起做用,而在 Release 版本中将被忽略。所以,在程序设计中应该避免在断言表达式中使用改变环境的语句。以下面的示例代码所示:

  1. int Test(int i)
  2. {
  3. assert(i++);
  4. return i;
  5. }
  6. int main(void)
  7. {
  8. int i=1;
  9. printf("%d\n",Test(i));
  10. return 0;
  11. }

对于上面的示例代码,因为“assert(i++)”语句的缘由,将致使不一样的编译版本产生不一样的结果。若是是在 Debug 版本中,由于这里向变量 i 所赋的初始值为 1,因此在执行“assert(i++)”语句的时候将经过条件检查,进而继续执行“i++”,最后输出的结果值为 2;若是是在 Release 版本中,函数中的断言语句“assert(i++)”将被忽略掉,这样表达式“i++”将得不到执行,从而致使输出的结果值仍是 1。

所以,应该避免在断言表达式中使用相似“i++”这样改变环境的语句,使用以下代码进行替换:

  1. int Test(int i)
  2. {
  3. assert(i);
  4. i++;
  5. return i;
  6. }

如今,不管是 Debug 版本,仍是 Release 版本的输出结果都将为 2。

避免使用断言去检查程序错误

在对断言的使用中,必定要遵循这样一条规定:对来自系统内部的可靠的数据使用断言,对于外部不可靠数据不可以使用断言,而应该使用错误处理代码。换句话说,断言是用来处理不该该发生的非法状况,而对于可能会发生且必须处理的状况应该使用错误处理代码,而不是断言。

在一般状况下,系统外部的数据(如不合法的用户输入)都是不可靠的,须要作严格的检查(如某模块在收到其余模块或链路上的消息后,要对消息的合理性进行检查,此过程为正常的错误检查,不能用断言来实现)才能放行到系统内部,这至关于一个守卫。而对于系统内部的交互(如子程序调用),若是每次都去处理输入的数据,也就至关于系统没有可信的边界,这样会让代码变得臃肿复杂。事实上,在系统内部,传递给子程序预期的恰当数据应该是调用者的责任,系统内的调用者应该确保传递给子程序的数据是恰当且能够正常工做的。这样一来,就隔离了不可靠的外部环境和可靠的系统内部环境,下降复杂度。

可是在代码编写与测试阶段,代码极可能包含一些意想不到的缺陷,也许是处理外部数据的程序考虑得不够周全,也许是调用系统内部子程序的代码存在错误,形成子程序调用失败。这个时候,断言就能够发挥做用,用来确诊究竟是哪部分出现了问题而致使子程序调用失败。在清理全部缺陷以后,就创建了内外有别的信用体系。等到发行版的时候,这些断言就没有存在的必要了。所以,不能用断言来检查最终产品确定会出现且必须处理的错误状况。

看下面一段示例代码:

  1. char * Strdup(const char * source)
  2. {
  3. assert(source != NULL);
  4. char * result=NULL;
  5. size_t len = strlen(source) +1;
  6. result = (char *)malloc(len);
  7. assert(result != NULL);
  8. strcpy(result, source);
  9. return result;
  10. }

对于上面的 Strdup 函数,相信你们都不陌生。其中,第一个断言语句“assert(source!=NULL)”用来检查该程序正常工做时绝对不该该发生的非法状况。换句话说,在调用代码正确的状况下传递给 source 参数的值必然不为 NULL,若是断言失败,说明调用代码中有错误,必须修改。所以,它属于断言的正常使用状况。

而第二个断言语句“assert(result!=NULL)”的用法则不一样,它测试的是错误状况,是在其最终产品中确定会出现且必须对其进行处理的错误状况。即对 malloc 函数而言,当内存不足致使内存分配失败时就会返回 NULL,所以这里不该该使用 assert 宏进行处理,而应该使用错误处理代码。以下面问题将使用 if 判断语句进行处理:

  1. char * Strdup(const char * source)
  2. {
  3. assert(source != NULL);
  4. char * result=NULL;
  5. size_t len = strlen(source)+1;
  6. result = (char *)malloc(len);
  7. if (result != NULL)
  8. {
  9. strcpy(result, source);
  10. }
  11. return result;
  12. }

总之记住一句话:断言是用来检查非法状况的,而不是测试和处理错误的。所以,不要混淆非法状况与错误状况之间的区别,后者是必然存在且必定要处理的。

尽可能在防错性程序设计中使用断言来进行错误报警

对于防错性程序设计,相信有经验的程序员并不陌生,大多数教科书也都鼓励程序员进行防错性程序设计。在程序设计过程当中,总会或多或少产生一些错误,这些错误有些属于设计阶段隐藏下来的,有些则是在编码中产生的。为了不和纠正这些错误,可在编码过程当中有意识地在程序中加进一些错误检查的措施,这就是防错性程序设计的基本思想。其中,它又能够分为主动式防错程序设计和被动式防错程序设计两种。

主动式防错程序设计是指周期性地对整个程序或数据库进行搜查或在空闲时搜查异常状况。它既能够在处理输入信息期间使用,也能够在系统空闲时间或等待下一个输入时使用。以下面所列出的检查均适合主动式防错程序设计。

  • 内存检查:若是在内存的某些块中存放了一些具备某种类型和范围的数据,则可对它们作常常性检查。
  • 标志检查:若是系统的状态是由某些标志指示的,可对这些标志作单独检查。
  • 反向检查:对于一些从一种代码翻译成另外一种代码或从一种系统翻译成另外一种系统的数据或变量值,能够采用反向检查,即利用反向翻译来检查原始值的翻译是否正确。
  • 状态检查:对于某些具备多个操做状态的复杂系统,若用某些特定的存储值来表示这些状态,则可经过单独检查存储值来验证系统的操做状态。
  • 链接检查:当使用链表结构时,可检查链表的链接状况。
  • 时间检查:若是已知道完成某项计算所需的最长时间,则可用定时器来监视这个时间。
  • 其余检查:程序设计人员可常常仔细地对所使用的数据结构、操做序列和定时以及程序的功能加以考虑,从中获得要进行哪些检查的启发。


被动式防错程序设计则是指必须等到某个输入以后才能进行检查,也就是达到检查点时才能对程序的某些部分进行检查。通常所要进行的检查项目以下:

  • 来自外部设备的输入数据,包括范围、属性是否正确。
  • 由其余程序所提供的数据是否正确。
  • 数据库中的数据,包括数组、文件、结构、记录是否正确。
  • 操做员的输入,包括输入的性质、顺序是否正确。
  • 栈的深度是否正确。
  • 数组界限是否正确。
  • 表达式中是否出现零分母状况。
  • 正在运行的程序版本是不是所指望的(包括最后系统从新组合的日期)。
  • 经过其余程序或外部设备的输出数据是否正确。


虽然防错性程序设计被誉为有较好的编码风格,一直被业界强烈推荐。但防错性程序设计也是一把双刃剑,从调试错误的角度来看,它把原来简单的、显而易见的缺陷转变成晦涩的、难以检测的缺陷,并且诊断起来很是困难。从某种意义上讲,防错性程序设计隐瞒了程序的潜在错误。

固然,对于软件产品,但愿它越健壮越好。可是调试脆弱的程序更容易帮助咱们发现其问题,由于当缺陷出现的时候它就会当即表现出来。所以,在进行防错性程序设计时,若是“不可能发生”的事情的确发生了,则须要使用断言进行报警,这样,才便于程序员在内部调试阶段及时对程序问题进行处理,从而保证发布的软件产品具备良好的健壮性。

一个很常见的例子就是无处不在的 for 循环,以下面的示例代码所示:

  1. for(i=0;i<count;i++)
  2. {
  3. /*处理代码*/
  4. }

在几乎全部的 for 循环示例中,其行为都是迭代从 0 开始到“count-1”,所以,你们也都天然而然地编写成了上面这种防错性版本。但存在的问题是:若是 for 循环中的索引 i 值确实大于 count,那么极有可能意味着代码中存在着潜在的缺陷问题。

因为上面的 for 循环示例采用了防错性程序设计方式,所以,就算是在内部测试阶段中出现了这种缺陷也很难发现其问题的所在,更加不可能出现系统报警提示。同时,由于这个潜在的程序缺陷,极有可能会在之后让咱们吃尽苦头,并且很是难以诊断。

那么,不采用防错性程序设计会是什么样子呢?以下面的示例代码所示:

  1. for(i=0;i!=count;i++)
  2. {
  3. /*处理代码*/
  4. }

很显然,这种写法确定是不行的,当 for 循环中的索引 i 值确实大于 count 时,它仍是不会中止循环。

对于上面的问题,断言为咱们提供了一个很是简单的解决方法,以下面的示例代码所示:

  1. for(i=0;i<count;i++)
  2. {
  3. /*处理代码*/
  4. }
  5. assert(i==count);

不难发现,经过断言真正实现了一箭双雕的目的:健壮的产品软件和脆弱的开发调试程序,即在该程序的交付版本中,相应的程序防错代码能够保证当程序的缺陷问题出现的时候,用户能够不受损失;而在该程序的内部调试版本中,潜在的错误仍然能够经过断言预警报告。

所以,“不管你在哪里编写防错性代码,都应该尽可能确保使用断言来保护这段代码”。固然,也没必要过度拘泥于此。例如,若是每次执行 for 循环时索引 i 的值只是简单地增 1,那么要使索引i的值超过 count 从而引发问题几乎是不可能的。在这种状况下,相应的断言也就没有任何存在的意义,应该从程序中删除。可是,若是索引 i 的值有其余处理状况,则必须使用断言进行预警。因而可知,在防错性程序设计中是否须要使用断言进行错误报警要视具体状况而定,在编码以前都要问本身:“在进行防错性程序设计时,程序中隐瞒错误了吗?”若是答案是确定的,就必须在程序中加上相应的断言,以此来对这些错误进行报警。不然,就不要画蛇添足了。

用断言保证没有定义的特性或功能不被使用

在平常软件设计中,若是原先规定的一部分功能还没有实现,则应该使用断言来保证这些没有被定义的特性或功能不被使用。例如,某通讯模块在设计时,准备提供“无链接”和“链接”这两种业务。但当前的版本中仅实现了“无链接”业务,且在此版本的正式发行版中,用户(上层模块)不该产生“链接”业务的请求,那么在测试时可用断言来检查用户是否使用了“链接”业务。以下面的示例代码所示:

  1. /*无链接业务*/
  2. #define CONNECTIONLESS 0
  3. /*链接业务*/
  4. #define CONNECTION 1
  5. int MessageProcess(MESSAGE *msg)
  6. {
  7. assert(msg != NULL);
  8. unsigned char service;
  9. service = GetMessageService(msg);
  10. /*使用断言来检查用户是否使用了“链接”业务*/
  11. assert(service != CONNECTION);
  12. /*处理代码*/
  13. }

谨慎使用断言对程序开发环境中的假设进行检查

在程序设计中,不可以使用断言来检查程序运行时所需的软硬件环境及配置要求,它们须要由专门的处理代码进行检查处理。而断言仅可对程序开发环境(OS/Compiler/Hardware)中的假设及所配置的某版本软硬件是否具备某种功能的假设进行检查。例如,某网卡是否在系统运行环境中配置了,应由程序中正式代码来检查;而此网卡是否具备某设想的功能,则能够由断言来检查。

除此以外,对编译器提供的功能及特性的假设也可使用断言进行检查,以下面的示例代码所示:

  1. /*int类型占用的内存空间是否为2*/
  2. assert(sizeof(int)== 2);
  3. /*long类型占用的内存空间是否为4*/
  4. assert(sizeof(long)==4);
  5. /*byte的宽度是否为8*/
  6. assert(CHAR_BIT==8);

之因此能够这样使用断言,那是由于软件最终发行的 Release 版本与编译器已没有任何直接关系。最后,必须保证软件的 Debug 与 Release 两个版本在实现功能上的一致性,同时可使用调测开关来切换这两个不一样的版本,以便统一维护,切记不要同时存在 Debug 版本与 Release 版本两个不一样的源文件。固然,由于频繁调用 assert 会极大影响程序的性能,增长额外的开销。所以,应该在正式软件产品(即 Release 版本)中将断言及其余调测代码关掉(尤为是针对自定义的断言宏)。