assert从初识到各种奇思妙想

assert从初识到各种奇思妙想

简介

C 标准库的 assert.h头文件提供了一个名为 assert 的宏,它可用于验证程序做出的假设,并在假设为假时输出诊断消息。

了解:已定义的宏 assert 指向另一个宏 NDEBUG,宏 NDEBUG 不是 <assert.h> 的一部分。如果已在引用 <assert.h> 的源文件中定义 NDEBUG 为宏名称,则 assert 宏的定义如下:

#define assert(ignore) ((void)0)

声明

下面是 assert() 宏的声明。

void assert(int expression);

参数

expression – 这可以是一个变量或任何 C 表达式。如果 expression 为 TRUE,assert() 不执行任何动作。如果 expression 为 FALSE,assert() 会在标准错误 stderr 上显示错误消息,并中止程序执行。

返回值

这个宏不返回任何值

assert运用

1、检验参数是否合理

如:

int resetBufferSize(int nNewSize) 
{ 
//功能:改变缓冲区大小, 
//参数:nNewSize 缓冲区新长度 
//返回值:缓冲区当前长度 
//说明:保持原信息内容不变 nNewSize<=0表示清除缓冲区 
assert(nNewSize >= 0); 
assert(nNewSize <= MAX_BUFFER_SIZE); 
... 
}

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

2、不能使用改变环境的语句

因为assert只在DEBUG个生效,如果这么做,会使用程序在真正运行时遇到问题

错误示例: assert(i++ < 100)

这是因为如果出错,比如在执行之前i=100,那么这条语句就不会执行,那么i++这条命令就没有执行。

正确示例:
assert(i < 100)
i++;

3、每个assert只检验一个条件

因为同时检验多个条件时,如果断言失败,无法直观的判断是哪个条件失败

4、assert和后面的语句应空一行,以形成逻辑和视觉上的一致感

5、assert比条件过滤更简洁

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;
}

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

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

void *MemCopy(void *dest, const void *src, size_t len)
{
    assert(dest != NULL);
    assert(src!=NULL);
    char *tmp_dest = (char *)dest;
    char *tmp_src = (char *)src;
    while(len --)
            *tmp_dest ++ = *tmp_src ++;
    return dest;
}

6、将断言用作可执行代码注释

断言可以生成极好的注释!编写出色的表达式可以确切地告诉开发人员在代码的某个给定点应该预料发生什么事情。开发人员应该做好他们断言的架构,帮助人们更清楚地理解系统中发生的事情,进而帮助减少缺陷。

7、assert 只有在 Debug 版本中才有效,如果编译为 Release 版本则被忽略。

以下是使用断言的几个原则:

(1)使用断言捕捉不应该发生的非法情况。不要混淆非法情况与异常情况之间的区别,后者是必然存在的并且是一定要作出处理的。如输入数据异常,请重新输入。
(2)在编写函数时,要进行反复的考查,并且自问:"我打算做哪些假定?"一旦确定了的假定,就要使用断言对假定进行检查。
(3)一般教科书都鼓励程序员们进行防错性的程序设计,但要记住这种编程风格会隐瞒错误。当进行防错性编程时,如果"不可能发生"的事情的确发生了,则要使用断言进行报警。
一个很常见的例子就是无处不在的 for 循环,如下面的示例代码所示:

for(i=0;i<count;i++)
{
    /*处理代码*/
}

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

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

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

for(i=0;i!=count;i++)
{
    /*处理代码*/
}

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

对于上面的问题,断言为我们提供了一个非常简单的解决方法,如下面的示例代码所示:


```c
for(i=0;i<count;i++)
{
    /*处理代码*/
}
assert(i==count);


拓展

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

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

/*使用断言测试*/
#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();
}

如果定义了 DEBUG,ASSERT 将被扩展为一个if语句,否则执行“#define ASSERT(condition) NULL”替换成 NULL。

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


```c
#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 所示。

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

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

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


```c
void Test(unsigned char *str)
{
    assert(str != NULL);
    /*函数处理代码*/
}

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

在这里插入图片描述

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

从上面的示例中不难发现,对标准的 assert 宏来说,自定义的 ASSERT 宏将具有更大的灵活性,可以根据自己的需要打印输出不同的信息,同时也可以对不同类型的错误或者警告信息使用不同的断言,这也是在工程代码中经常使用的做法。当然,如果没有什么特殊需求,还是建议使用标准 assert 宏。


原文链接:C语言断言assert-从源码解析到熟练使用_c assert-CSDN博客

断言(assert)的用法 | 菜鸟教程 (runoob.com)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值