程序中的断言(ASSERT)

程序中的断言(ASSERT)

概念

断言(assertion)是一种防御式除错机制,用于验证代码是否符合编码人员的预期。编码人员在开发期间应该对函数的参数、代码中间执行结果合理地使用断言机制,确保程序的缺陷尽量在测试阶段被发现。

通常来说,断言并不是正常程序所必需的,但对于程序调试来说,断言能够快速定位那些违反了某些前提条件的程序错误。

断言就是将一个表达式放在语句中,用以排除在设计逻辑上不应该出现的情况。其作用类似于以下的if语句:

if(判别表达式成立)
{
	程序正常运行;
}
else
{
	报错&&终止程序!(避免由程序运行引起更大的错误)  
}
  • 在C语言中,头文件<assert.h>提供了assert
  • 在C++中,头文件<cassert>提供了assert

用法

断言(assert)在C/C++中是用宏来实现的,原型如下:

#include <assert.h>
void assert( int expression );

assert 的作用是计算表达式 expression ,如果其值为假(即为0),那么它先向 stderr 打印一条出错信息,然后通过调用 abort 来终止程序运行。【即为真则继续,为假则报错退出】

assert会频繁地调用,极大影响程序性能,因此一般只在debug下使用,在release版的程序中#include <assert.h> 语句之前插入 #define NDEBUG 来禁用assert调用,即ASSERT只有在Debug版本中才有效,如果编译为Release版本则被忽略。

#include <stdio.h>
#define NDEBUG
#include <assert.h>

用法总结与注意事项

  1. 在函数开始处检验传入参数的合法性

    int resetBufferSize(int nNewSize) {
    //功能:改变缓冲区大小,
    //参数:nNewSize 缓冲区新长度
    //返回值:缓冲区当前长度
    //说明:保持原信息内容不变     nNewSize<=0表示清除缓冲区
    assert(nNewSize >= 0);
    assert(nNewSize <= MAX_BUFFER_SIZE);
    ...
    }
    
  2. 每个assert只检验一个条件,因为同时检验多个条件时,如果断言失败,无法直观的判断是哪个条件失败

    // 不好: 
    assert(nOffset>=0 && nOffset+nSize<=m_nInfomationSize);  
    // 好: 
    assert(nOffset >= 0); 
    assert(nOffset+nSize <= m_nInfomationSize); 
    
  3. 不能使用改变环境的语句,因为assert只在DEBUG下生效,如果这么做,会使用程序在真正运行时遇到问题(产生副作用),导致 debug 和 release 版的行为不一样

    错误: assert(i++ < 100);
    //这是因为如果出错,比如在执行之前i=100,那么这条语句就不会执行,那么i++这条命令就没有执行。 
    正确: assert(i < 100); i++; 
    
  4. 初学者可能会难于分辨何时使用断言,何时处理运行时错误(如返回错误值或抛出异常)。简单的答案是,如果那个错误是由于程序员错误编码所造成的(例如传入不合法的参数),那么应用断言;如果那个错误是程序员无法避免,而是由运行时的环境所造成的,就要处理运行时错误(例如开启文件失败)。

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

  6. 有的地方,assert不能代替if-else条件过滤,具体原因见下文assert和if-else的选择

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

  1. 使用断言捕捉不应该发生的非法情况。不要混淆非法情况与错误情况之间的区别,后者是必然存在的并且是一定要作出处理的。

  2. 使用断言对函数的参数进行确认。

  3. 在编写函数时,要进行反复的考查,并且自问:“我打算做哪些假定?”一旦确定了的假定,就要使用断言对假定进行检查。

  4. 一般教科书都鼓励程序员们进行防错性的程序设计,但要记住这种编程风格会隐瞒错误。当进行防错性编程时,如果“不可能发生”的事情的确发生了,则要使用断言进行报警。

示例

示例1:

以下代码中对除数使用了断言,当除数为0时程序会报错。

/*test4.cpp*/
#include <iostream>
#include <cassert>
using namespace std;

double Sub(int dividend, int divisor) {
    assert(divisor != 0);  //断言,除数必须大于0
    return static_cast<double>(dividend) / static_cast<double>(divisor);
}

int main() {
    int a = 6, b = 3, c = 0;
    cout << Sub(b, c);
    return 0;
}

运行结果:

Assertion failed!

Program: G:\Code\test4.exe
File: test4.cpp, Line 6

Expression: divisor != 0

示例2:

/*test4.cpp*/

#include <assert.h>  //需要包含的头文件,release版本去掉,测试时使用
#include <stdio.h>
/* assert 的使用 */
#define DEBUG  // release 版本注释掉即可,测试版本定义
#ifdef DEBUG
#define ASSERT(f) assert(f)
#else
#define ASSERT(f) ((void)0)
#endif

void display(int n) {
    ASSERT(n > 0);
    printf("%u", n);
}

int main() {
    unsigned int x;
    scanf("%u", &x);
    display(x);
    return 0;
}

运行结果:

Assertion failed!

Program: G:\Code\test4.exe
File: test4.cpp, Line 14

Expression: n > 0

assert和if-else的选择

参考:代码安全性和健壮性:如何在if和assert中做选择? - 知乎 (zhihu.com)

首先声明一点:这 2 种检查方式,在实际的代码中都很常见,从功能上来说似乎也没有什么影响。因此,没有严格的错与对之分,很多都是依赖于每个人的偏好习惯不同而已。

(1) assert 支持者

我作为 my_concat() 函数的实现者,目的是拼接字符串,那么传入的参数必须是合法有效的,调用者需要负责这件事。如果传入的参数无效,我会表示十分的惊讶!怎么办:崩溃给你看!

(2)if 支持者

我写的 my_concat() 函数十分的健壮,我就预料到调用者会乱搞,故意的传入一些无效参数,来测试我的编码水平。没事,来吧,我可以处理任何情况!

这两个派别的理由似乎都很充足!那究竟该如何选择?难道真的的跟着感觉走吗?

假设我们严格按照常规的流程去开发一个项目:

  1. 在开发阶段,编译选项中不定义 NDEBUG 这个宏,那么 assert 就发挥作用;
  2. 项目发布时,编译选项中定义了 NDEBUG 换个宏,那么 assert 就相当于空语句;

也就是说,只有在 debug 开发阶段,用 assert 断言才能够正确的检查到参数无效。而到了 release 阶段,assert 不起作用,如果调用者传递了无效参数,那么程序只有崩溃的命运了。

这说明什么问题?是代码中存在 bug?还是代码写的不够健壮?

从我个人的理解上看,这压根就是单元测试没有写好,没有测出来参数无效的这个 case!

小结

不允许:就用 assert 断言,在开发阶段就尽量找出所有的错误情况;

允许:就用 if-else,说明这是一个合理的逻辑,需要进行下一步处理。

assert 的本质

assert 就是为了验证有效性,它最大作用就是:在开发阶段,让我们的程序尽可能地 crash。每一次的 crash,都意味着代码中存在着 bug,需要我们去修正。

当我们写下一个 assert 断言的时候,就说明:断言失败的这种情况是不可以的,是不被允许的。必须保证断言成功,程序才能继续往下执行。

if-else 的本质

if-else 语句用于逻辑处理,它是为了处理各种可能出现的情况。就是说:每一个分支都是合理的,是允许出现的,我们都要对这些分支进行处理。

posted @ 2023-02-04 13:23  3的4次方  阅读(29)  评论(0编辑  收藏  举报