断言的魅力
当你写好了一段代码后,会如何调试该程序呢?有些人可能习惯于各种集成环境下的调试,或者GDB这种现成的调试工具,亦或是使用printf来“暴力”打印调试信息。那么,今天我将介绍一种新的调试方式——断言(也许对于你已经烂熟于心了)。
当然说是新也不新,在《编程珠玑》第二版的第五章介绍了断言的用法,我们来看一看断言到底如何使用。断言其实是用来判断某个逻辑表达式是否为真的语句,在C中以assert函数实现,我们需要将断言插入代码,以确保程序运行时的行为与我们的理解一致。我们来举个例子:(C代码需要包含头文件assert.h)
#include <stdio.h>
#include <assert.h>
int main(void)
{
int a = 1;
int b = 2;
assert(a > b);
return 0;
}
在linux下使用GCC编译后,运行该程序,得到的信息:
asset: asset.c:8: main: Assertion `a > b' failed.
已放弃 (核心已转储)
我们可以看到打印信息显示,a > b错误。也就是说assert判断输入的表达式是否为真,如果为真则不显示任何信息,如果不真则提示错误并退出程序。所以,我们可以在某些我们觉得可能会出错误的地方放上断言来调试程序。
其实我们还是糊里糊涂,断言到底该如何用,与其他的调试到底有什么区别呢?
《代码大全》第二版第八章列出了什么情况下使用断言:
- 输入参数或输出参数的取值处于预期的范围内;
- 子程序开始(或者结束)执行时文件或流是处于打开(或关闭)的状态;
- 子程序开始(或者结束)执行时,文件或流的读写位置处于开头(或结尾);
- 文件或流已用只读,只写或者可读可写方式打开;
- 仅用于输入的变量的值没有被子程序修改;
- 指针非空;
- 传入子程序的数组或其他容器至少能容纳X个数据元素;
- 表初始化,存储着真实的数值;
- 子程序开始(或者结束)执行时,某个容器是空的(或满的);
- 一个经过高度优化的复杂子程序的运算结果和相对缓慢但代码清晰的子程序的运算结果相一致;
当然,这里只是举出了一些基本假定,我们来看看《代码大全》对使用断言的指导建议:
- 用错误处理代码来处理预期会发生的情况,用断言来处理不应该发生的状况
断言用来检查永远不该发生的情况,而错误代码处理代码使用来检查不太可能发生的非正常情况。错误处理通常用来检查有害的输入数据,而断言是用于检查代码中的bug。
举例来说,比如代码要求你输入一个正数,你可能会输入负数,这当然有可能发生,这时应该使用错误处理代码。但是,比如数组越界可能是你程序本身的错误,是不应该发生,这时可以用断言来检查。
- 用断言来注解并验证前条件和后条件
简单来说,前条件是程序执行前应该保证为真的部分,而后条件是程序执行结束后应确保为真的部分。比如某些输入应该为真,否则会出错。而正确的输入应该产生正确的输出。比如输入是个偶数,输出应该是单数,我们需要验证输入是偶数,输出是奇数。
- 对于高健壮性的代码,应该先使用断言再处理错误
由于有些代码异常庞大复杂,可能会经历不同的程序员,或很长的开发周期。故不同人对于某段程序谨慎程度是不一样的。也许他会调试某段代码,也许只会提供错误处理。故此时,先使用断言再处理错误,两者一齐使用则是非常有效的方法。
总结来说,也就是在你觉得可能会出现bug的地方加入断言,如《编程珠玑》中描述的一样,可以测试很多输入是否产生正确的输出。
《代码大全》提供了一种更好的断言实现,使用宏即可:
#define ASSERT(condition, message) { \
if (! (condition) ) { \
printf("Assertion failed: ",); \
#condition, message ); \
exit( EXIT_FAILURE); \
} \
} \
这里对书中代码稍作改变,使用上面的宏,我们不仅可以判断表达式是否为真,也可以打印相关的错误信息。同时,这段宏在编译时通过预处理器,减少了额外的开销。
除此,我之所喜欢断言,因为它不像调试工具一样复杂(需要设置断点,查看寄存器等),也不会像pringf语句一样无论如何都会打印信息。断言是如此简洁而高效,是很多编程大牛都推荐使用的调试方法(《编程人生》,当然也有使用调试工具和printf的)。
这篇文章对断言的实现机制做了很好的解释(2013年7月13日加)。
blog comments powered by Disqus