众所周知,我们在实际开发C程序的时候,往往是编码容易——调试困难,修改容易——排查困难。我们在开发过程中,debug占据了我们很大一部分的时间,而正确地使用各种编码手段,可以有效地提升排查问题代码的效率。笔者从自己的实践经验出发,给大家分享一个用于编码/调试阶段高效发现问题代码的利器,这就是大名鼎鼎的**assert**。通过阅读本文,你将了解到以下内容:
assert它的中文含义是“断言”,它被包含在中,往往给使用者呈现的形式为: assert() 。因此,很多开发者认为它就是一个函数,可能它的原型就是void assert(int expression); 但研究过assert.h的,一定会发现,其实并不是。
assert的真身,其实是一个宏定义,只不过是一个带参数输入的宏定义,与我们之前一篇八卦Linux内核设计的max宏定义 (【Linux内核】从小小的宏定义窥探Linux内核的精妙设计)类似的。庐山真面目如下所示:
#define assert(e) ((e) ? (void)0 : _assert(#e, __FILE__, __LINE__))
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zc6EAp8U-1661923571346)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]
从它的定义,我们可以很清晰的知道,真正起到打印作用的是_assert,而它才是真正的一个函数。原型为:
void _assert(const char *e, const char *file, int line);
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dax1ldZf-1661923571352)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]
本文的主题是利用assert高效排查问题代码,自然assert的用途就是排查代码;但是,具体它的功能是怎么体现呢?假设有如下代码,一个测试函数的实现片段:
int test_function(int a, int *b)
{
assert(a > 1); /* 断言:入参a的值一定大于1 */
assert(b); /* 断言: 入参b指针一定不是NULL */
/* Do other things here ... */
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ejym7Jij-1661923571353)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]
如代码所示,有一个测试函数test_function,接收2个入参,一个是int型的a变量,一个int *类型的b指针;在函数的开始,我们就用assert分别对a和b做了断言,确保它们有正确的输入。假设我们有如下的函数调用的测试代码:
{
int a = 7;
int *b = &a;
test_function(a, b);
/* Do other things here ... */
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E4QQ143R-1661923571355)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]
很明显,当如上代码调用test_fucntion时,内部的两个assert判断均为【真】,那么什么事情也不会发生,assert就像一个优雅的淑女,静静地站在那里看着你。
当我们的测试代码做如下调整:
{
int a = 0;
int *b = &a;
test_function(a, b);
/* Do other things here ... */
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qPbDKDBb-1661923571357)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]
很明显,test_function的第一个assert语句不为【真】,那么它就像山洪一样要爆发了,终止程序运行的同时,会输出类似的错误提示: Assertion failed: a > 1,file xxx.c, line 128,这段错误提示,不仅告诉了我们哪个条件判断出错了,并且还告诉了我们出错的位置在哪个文件的哪一行,这是多么智能啊!由此可见,它真正的威力在于【代码出错】时,即当代码没有按照我们的断言进行时,我们就应该停下来,排查下为何会有错误的参数输入,这样我们就可以将bug在出现苗头的时候就把它消灭掉。
其实,上面的示例代码已经展示了如何使用assert,但是我们需要补充的是,一般在使用assert断言语句的时候,需要在对应的.c文件加上对assert.h的引用,否则编译会报错误。
assert这么智能的利器是非常有利于我们写出高质量不易出错的代码的,通常富有经验的程序员都会很擅长使用assert语句,把assert打在恰当的语句中,可以最大限度地提升我们的代码质量。但是,很多开发者开始有疑惑了,要是每条语句,每个判断都加上assert,那么就算全部assert的情况都是【真】,也够CPU忙一会了,这样似乎有些浪费CPU的计算能力,以追求高效的C语言编程,可容不下这样的事情发生。那,这可怎么办呢?
为避免以上情况的发生,我们作为assert的使用者,一般只需要在开发调试阶段才使用assert,而在正式发布的版本是需要去掉assert的。这样疑惑就更大了,发布版本一条条删掉assert调用,万一删错了代码呢?设计者总是聪明的,他们也早就想到了这一点,这不他们也提供了解决方案。开头的时候,我们介绍了assert是一个宏,但并没有完全展示它的全貌。现在开始展示它的真容:
#ifdef NDEBUG
#define assert(e) (void)0
#else
#define assert(e) ((e) ? (void)0 : _assert(#e, __FILE__, __LINE__))
#endif
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-69cPczpO-1661923571359)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]
聪明的你,一定也发现了,我们只需要在.c文件#include 之前,加上一句#define NDEBUG 1就可以把相应.c中的assert(e)全部变成((void)0);而((void)0)本身是个无效调用代码,在实际的编译过程中是会被优化掉的,这样我们仅增加对NDEBUG(NO DEBUG的意思)的宏定义,就可以把全部的assert给摒弃了,是不是很智能呢?
正如上面所述,assert这么智能,但是我们也不能滥用,只需要在恰当的位置作为特定的判断;通常来说,我们有以下一些情景可以考虑使用assert语句:
综述,assert是把双刃剑,出错时它能很优秀地暴露问题代码,非常有利于我们排查代码,从而以最快的速度找到问题并解决问题;同时,它的频繁调用,一定程度上加上了CPU的处理,做一些无畏的判断,“简直就是在浪费生命”。所以,在实际开发过程中,我们务必要严谨细致地使用assert,让它更好地为我们服务。
只有不自负且思维严谨的人才能使用好assert,我们只有做到了不自负,不对自己的代码打100%的包票,相信是代码总会有出错的时候,才会逐步养成思维严谨的习惯,反而对自己的代码质量有更大的提升。
本文对assert的介绍和使用做了一番总结,文中难免有纰漏之处,还望读者诚心指正,感谢。
全部0条评论
快来发表一下你的评论吧 !