C语言陷阱与技巧20节,自定义“编译时”assert方法

在C语言程序开发中,程序员写代码时应该考虑的“面面俱到”,这样才能写出功能稳定的程序。例如,在实现 open() 函数时,先完成它的功能固然是重要的,但是程序员还需要考虑各种“意外”,比如下面这种情况。

假设不存在 /dev/sth 这个文件,仍然调用 open() 函数打开它:

int fd = open("/dev/sth", O_RDONLY);

此时 open() 函数不应该感到迷惑,而是具备处理这种“意外”的能力。标准库的 open() 函数在遇到这种情况时,会返回一个错误码,对应着“文件不存在”的错误信息。

所以我们在开发C语言程序的过程中,写出的代码也应具备这种处理“意外”的能力。处理“意外”最常用的方式之一就是返回一个错误码,输出一段错误提示信息,这一点其实之前的文章讨论过。

使用 assert

在C语言程序开发阶段,为了方便,我们可以在可能出现不预期的“意外”处使用 assert()。assert() 的C语言原型如下:

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

使用它需要包含 assert.h,assert() 接收一个参数 expression,可以是一个表达式,如果 expression 为真,则什么都不会发生。如果 expression 为假,则 assert() 会终止C语言程序,并且输出 assert 失败的代码位置。

例如下面这段C语言代码:

int fd = open("/dev/sth", O_RDONLY);
assert(fd > 0);
printf("fd = %d\n", fd);


编译并执行,得到如下结果:

# gcc t.c
# ./a.out 
a.out: t.c:11: main: Assertion `fd > 0' failed.
Aborted

可以看出,第 12 行的 printf() 函数并没有被执行。这是因为程序运行环境里并没有 "/dev/sth" 这个文件,所以 open() 函数执行失败,传递给 assert() 的参数为假,C语言程序被终止,并且输出 t.c 源文件第 11 行代码 assert 失败。

assert() 可以输出出错的代码位置,这个特性在较为大型的C语言程序开发中是非常好用的,因为无需程序员再去手工调试代码,排查出错代码的位置了。

不过,assert() 在遇到假参数时,直接将C语言程序终止太过于死板。比如某个C语言程序有两套逻辑,第一套逻辑在 open() 函数成功打开文件时运行,第二套逻辑则在 open() 函数打开文件失败时运行。要是使用 assert() 判断 open() 函数是否成功打开文件,则第二套逻辑永远没有机会运行。

所以,assert() 一般仅用于开发阶段帮助程序员定位错误,不能依赖 assert() 处理“意外”。事实上,为了便于使用,在定义了 NDEBUG 宏之后,assert() 就不再生成代码了,此时 assert() 相当于一个空格。请看下面这段C语言代码:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define NDEBUG
#include <assert.h>
int main()
{
     int fd = open("/dev/sth", O_RDONLY);
     assert(fd > 0);
     printf("fd = %d\n", fd);
     return 0; 
} 


编译上述C语言代码并执行,得到如下输出:

# gcc t.c
# ./a.out 
fd = -1

编译时 assert

可以看出,assert() 用于处理C语言程序可能出现诸多预期之外的“意外”时很有用,它能够自己输出究竟哪一个“意外”发生。但是 assert() 也是死板的,它在遇到假条件时直接把程序终止,剩余的代码逻辑不再有机会执行。

另外还有一点要说明,assert() 本身也会影响C语言程序的运行效率,这也是它常常只被使用在开发阶段的另一个原因。

其实仔细想想,使用 assert() 的目的其实只是希望它能够在C语言程序遇到不预期的“意外”时提醒程序员,我们并不关心 assert() 是否参与程序运行。如果使用 assert() 判断的是常量表达式,那我们可以自己定义一个 static_assert() 宏,并且让它在编译时就判断条件表达式是否成立,这样的宏可能在某些场合更加好用。

那该如何实现编译时 assert 这个功能呢?

其实很简单,首先应该明白数组的长度不可能是负数,基于这一点,static_assert() 宏就容易实现了,请看下面的C语言代码:

#define static_assert(expr) \
     do{ char tmp[(expr)?1:-1]; }while(0)

如果条件表达式为真,则 static_assert() 宏会定义一个长度为 1 的数组,否则就会尝试定一个长度为 -1 的数组,此时必定无法编译通过。这里值得一提的一个小技巧是使用 {} 符号将定义的 tmp 数组的作用域限定在本次调用的 static_assert 宏里,避免多次调用 static_assert 时出现重复定义。

写出如下C语言代码测试之:

int main()           
 {                    
     static_assert(2>1);
     printf("assert 2>1\n");

     static_assert(2<1);
     printf("assert 2<1\n");
     return 0;        
 } 


编译这段C语言代码,得到如下输出:

显然,static_assert() 宏在编译阶段就将假条件表达式找出来了。可能有些读者会觉得如果 assert 成功,就会定义一个 tmp 数组,虽然它的长度很短,但是仍然浪费了栈空间。其实这里可以把长度为零的数组,即:

#define static_assert(expr) \
     do{ char tmp[(expr)?0:-1]; }while(0)

在 assert 成功时会执行 char tmp[0];,它的长度为 0,感兴趣的读者可以使用 sizeof() 测试一下。到这里,我们就较为粗略的定义好了 static_assert 宏,它在编译阶段就能发现假条件。

小结

本节主要介绍了 assert() 的使用,应该能够发现,在开发阶段,它能够帮助程序员快速的定位“意外”,也讨论了 assert() 的不足之处,并在此基础上自己定义了“编译时”的static_assert 宏。按照这样的思路,其实还有很多定义 static_assert() 宏的其他方法,具体哪些方法留给读者自己思考了。

阅读更多:   C语言
添加新评论

icon_redface.gificon_idea.gificon_cool.gif2016kuk.gificon_mrgreen.gif2016shuai.gif2016tp.gif2016db.gif2016ch.gificon_razz.gif2016zj.gificon_sad.gificon_cry.gif2016zhh.gificon_question.gif2016jk.gif2016bs.gificon_lol.gif2016qiao.gificon_surprised.gif2016fendou.gif2016ll.gif