程序中的断言(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>
用法总结与注意事项:
-
在函数开始处检验传入参数的合法性
int resetBufferSize(int nNewSize) { //功能:改变缓冲区大小, //参数:nNewSize 缓冲区新长度 //返回值:缓冲区当前长度 //说明:保持原信息内容不变 nNewSize<=0表示清除缓冲区 assert(nNewSize >= 0); assert(nNewSize <= MAX_BUFFER_SIZE); ... }
-
每个assert只检验一个条件,因为同时检验多个条件时,如果断言失败,无法直观的判断是哪个条件失败
// 不好: assert(nOffset>=0 && nOffset+nSize<=m_nInfomationSize); // 好: assert(nOffset >= 0); assert(nOffset+nSize <= m_nInfomationSize);
-
不能使用改变环境的语句,因为assert只在DEBUG下生效,如果这么做,会使用程序在真正运行时遇到问题(产生副作用),导致 debug 和 release 版的行为不一样
错误: assert(i++ < 100); //这是因为如果出错,比如在执行之前i=100,那么这条语句就不会执行,那么i++这条命令就没有执行。 正确: assert(i < 100); i++;
-
初学者可能会难于分辨何时使用断言,何时处理运行时错误(如返回错误值或抛出异常)。简单的答案是,如果那个错误是由于程序员错误编码所造成的(例如传入不合法的参数),那么应用断言;如果那个错误是程序员无法避免,而是由运行时的环境所造成的,就要处理运行时错误(例如开启文件失败)。
-
assert和后面的语句应空一行,以形成逻辑和视觉上的一致感
-
有的地方,assert不能代替if-else条件过滤,具体原因见下文assert和if-else的选择
以下是使用断言的几个原则:
-
使用断言捕捉不应该发生的非法情况。不要混淆非法情况与错误情况之间的区别,后者是必然存在的并且是一定要作出处理的。
-
使用断言对函数的参数进行确认。
-
在编写函数时,要进行反复的考查,并且自问:“我打算做哪些假定?”一旦确定了的假定,就要使用断言对假定进行检查。
-
一般教科书都鼓励程序员们进行防错性的程序设计,但要记住这种编程风格会隐瞒错误。当进行防错性编程时,如果“不可能发生”的事情的确发生了,则要使用断言进行报警。
示例
示例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的选择
首先声明一点:这 2 种检查方式,在实际的代码中都很常见,从功能上来说似乎也没有什么影响。因此,没有严格的错与对之分,很多都是依赖于每个人的偏好习惯不同而已。
(1) assert 支持者
我作为 my_concat()
函数的实现者,目的是拼接字符串,那么传入的参数必须是合法有效的,调用者需要负责这件事。如果传入的参数无效,我会表示十分的惊讶!怎么办:崩溃给你看!
(2)if 支持者
我写的 my_concat()
函数十分的健壮,我就预料到调用者会乱搞,故意的传入一些无效参数,来测试我的编码水平。没事,来吧,我可以处理任何情况!
这两个派别的理由似乎都很充足!那究竟该如何选择?难道真的的跟着感觉走吗?
假设我们严格按照常规的流程去开发一个项目:
- 在开发阶段,编译选项中不定义 NDEBUG 这个宏,那么 assert 就发挥作用;
- 项目发布时,编译选项中定义了 NDEBUG 换个宏,那么 assert 就相当于空语句;
也就是说,只有在 debug 开发阶段,用 assert 断言才能够正确的检查到参数无效。而到了 release 阶段,assert 不起作用,如果调用者传递了无效参数,那么程序只有崩溃的命运了。
这说明什么问题?是代码中存在 bug?还是代码写的不够健壮?
从我个人的理解上看,这压根就是单元测试没有写好,没有测出来参数无效的这个 case!
小结
不允许:就用 assert 断言,在开发阶段就尽量找出所有的错误情况;
允许:就用 if-else,说明这是一个合理的逻辑,需要进行下一步处理。
assert 的本质
assert 就是为了验证有效性,它最大作用就是:在开发阶段,让我们的程序尽可能地 crash。每一次的 crash,都意味着代码中存在着 bug,需要我们去修正。
当我们写下一个 assert 断言的时候,就说明:断言失败的这种情况是不可以的,是不被允许的。必须保证断言成功,程序才能继续往下执行。
if-else 的本质
if-else 语句用于逻辑处理,它是为了处理各种可能出现的情况。就是说:每一个分支都是合理的,是允许出现的,我们都要对这些分支进行处理。