读者应该都听说过assert断言。断言语句指定在程序的某些特定点应为真的条件。如果该条件不为真,则断言失败,中断程序的执行,在windows下会显示“断言失败”对话框,而linux会退出程序执行。
虽然assert断言的实现方式各种各样,不过assert宏大多数情况下和下面的定义相差悬殊不大:
#ifndef NDEBUG
#define assert(e) ((e) ? ((void)0) : __assert_failed(#e,__FILE__,__LINE__))
#else
#define assert(e) ((void)0)
#endif
如果NDEBUG有定义,那么我们就没有在调试模式下,assert宏就会展开成一个空操作(no-op)。否则,我们就处在调试模式下,(在此特定实现中)assert宏就会展开成一个条件表达式以对某特定条件进行(谓词)测试。若该条件测试结果为false,则我们生成一条诊断信息并调用abort(以无条件强制终止程序运行)。
使用assert宏优于使用注释来文档化前置条件、后置条件及不变量。一条assert宏,在生效时,会对执行特定条件来个运行时校验,所以不会被轻轻松松地被当作一个注释而被无视。与注释不同的是,由于违反了assert宏的正确性校验的错误通常来说都被更正了,因为“调用abort”这种后果会使得“代码需要维护”这件事必须马上完成。
断言的特征
- 前置条件断言:代码执行之前必须具备的特性
- 后置条件断言:代码执行之后必须具备的特性
- 前后不变断言:代码执行前后不能变化的特性
在C++中,assert 宏的原型定义在<assert.h>中,其作用是如果它的条件返回错误,则终止程序执行,原型定义:
#include <assert.h>
void assert( int expression );
assert 的作用是现计算表达式 expression ,如果其值为假(即为 0),那么它先向 stderr 打印一条出错信息,然后通过调用 abort 来终止程序运行。我们看下面的示例:
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
int main( void )
{
FILE *fp;
fp = fopen( "test.txt", "w" );
//以可写的方式打开一个文件,如果不存在就创建一个同名文件 assert( fp ); 所以这里不会出错
fclose( fp );
fp = fopen( "noexitfile.txt", "r" );//以只读的方式打开一个文件,如果不存在就打开文件失败
assert( fp ); //所以这里出错
fclose( fp ); //程序永远都执行不到这里来
return 0;
}
在VC++编译器下运行上述程序,程序会溢出结束,并抛出一个断言对话框。已放弃使用 assert 的缺点是,频繁的调用会极大的影响程序的性能,增加额外的开销。在调试结束后,可以通过在包含#include <assert.h>的语句之前插入 #define NDEBUG 来禁用 assert 调用,示例代码如下:
#include <stdio.h>
#define NDEBUG
#include <assert.h>
但是,C++中何时需要使用断言呢?什么地方不适合使用断言呢?什么地方使用断言的环境,可总结为:
- 可以在预计正常情况下程序不会到达的地方放置断言 :assert( false)。
- 断言可以用于检查传递给私有方法的参数。(对于公有方法,因为是提供给外部的接口,所以必须在方法中有相应的参数检验才能保证代码的健壮性)。
- 使用断言测试函数方法执行的前置条件和后置条件。
- 使用断言检查类的不变状态,确保任何情况下,某个变量的状态必须满足。(如 age 属性应大于 0 小于某个合适值)。
同样断言语句不是永远会执行,可以屏蔽也可以启用,什么地方不要使用断言,可总结为:
- 不要使用断言作为公共方法的参数检查,公共方法的参数永远都要执行。
- 断言语句不可以有任何边界效应,不要使用断言语句去修改变量和改变函数方法的返回值。
接下来讨论一下断言assert的用法。一般断言主要用于在函数开始处,检验传入参数的合法性,例如:
// 功能:改变缓冲区大小,
// 参数:nNewSize 缓冲区新长度,
// 返回值:缓冲区当前长度,
// 说明:保持原信息内容不变,nNewSize<=0 表示清除缓冲区
int resetBufferSize(int nNewSize)
{
assert(nNewSize >= 0);
assert(nNewSize <= MAX_BUFFER_SIZE);
...
}
当然,除了用在函数的开始出,检查含函数的参数外。Assert断言还可用于任何我们需要验证的地方。如判断一个变量的值是否满足你的期望等。
最佳实践:Assert断言可以给我们带来安全检查,如果不适用不慎也同样会给我们带来副作用。
(1)每个 assert 只检验一个条件,因为同时检验多个条件时,如果断言失败,无法直观的判断是哪个条件失败。
不好的做法:
assert(nOffset>=0 && nOffset+nSize<= nInfomationSize);
好的做法:
assert(nOffset >= 0);
assert(nOffset+nSize <= nInfomationSize);
(2)不能使用改变环境的语句,因为assert只在DEBUG 时生效,如果这么做,会使用程序在真正运行时遇到问题。
错误: assert(i++ < 100),如果出错,在执行之前 i = 100,这条语句就不会执行,i++这条命令就没有执行。
正确: assert(i < 100); i++;
(3)assert 和后面的语句应空一行,以形成逻辑和视觉上的一致感。
(4)在 switch 语句中总是default子句显示信息(Assert)。
int number = SomeMethod();
switch(number)
{
case 1:
Trace.WriteLine("Case 1:");
break;
case 2:
Trace.WriteLine("Case 2:");
break;
default :
assert(false);
break;
}
请谨记
- assert宏在位于注释和异常之间的某个位置扮演了代码文档化及捕捉非法行为的适当角色。
- assert最大问题在于它是一个伪函数,因此它也(无可避免地)带着实用经验56,实用经验57中描述的有关伪函数的种种先天不足。好在它是一个标准化了的伪函数,这也就暗示着其不足之处已为世人熟知。只要使用时多长个心眼,assert宏就能为我们造福。