分享

这两个C语言宏有人说没用,其实作用很大,也许是他们不知道而已

 挑燈看劍r7wtm5 2019-07-19

我的上一篇文章讨论了 Linux 内核C语言源码中的两个宏:

#define BUILD_BUG_ON_ZERO(e) (sizeof(struct { int:-!!(e); }))#define BUILD_BUG_ON_NULL(e) ((void *)sizeof(struct { int:-!!(e); }))

这两个C语言宏有人说没用,其实作用很大,也许是他们不知道而已

Linux 内核C语言源码中的两个宏

如果读者看了上一篇文章,应该已经明白这两个宏具备编译时检查C语言代码的功能,能够帮助程序员在程序运行之前发现错误。

这两个宏有实用价值吗

这两个C语言宏有实用价值吗

BUILD_BUG_ON_XX 宏只能判断常量表达式,似乎没有什么应用价值,毕竟两个常量的对比谁会弄错呢?有读者(@Gerafore)不知道哪种场合用的是,甚至还有读者(@帖木兒)认为这样的宏根本就没有什么用处。

其实 BUILD_BUG_ON_XX 宏就相当于计算器,1 与 0 对比没人弄错,若是常量表达式在复杂一些呢?例如 1234 * 4321 和 2223 * 2322 的对比,如果再复杂些呢?要程序员自己手动去计算这些,就有些丢失编程的意义了,毕竟编程本来是希望计算机处理这些计算的。

另外,在实际的C语言程序开发中,通常都会用到大量的宏,要是每次用到宏,都去翻一翻它的值,再手动计算对比该有多烦啊!

手动计算对比该有多烦啊

BUILD_BUG_ON_XX 宏的应用还有很多,例如它还可以和一些C语言编译器内置函数结合使用,比如 __builtin_types_compatible_p() 函数,它接收两个数据类型,如果两个数据类型相同,则返回 1,否则返回 0。有趣的是,__builtin_types_compatible_p() 函数返回值是常量,这就为使用类似于BUILD_BUG_ON_XX 的宏提供了条件。

实例

我之前有文章曾讨论过如何使用 sizeof() 关键字计算C语言数组长度:

#define arr_len(arr) (size_t)(sizeof(arr)/sizeof(*arr))

但是需要注意的是,若想 arr_len 宏能够正确计算数组长度,只能传递给它数组名,但是C语言中的数组和指针关系暧昧,很难保证程序员不会误传指针给 arr_len() 宏,例如下面这段C语言代码:

void fun(char a[]){ ... size_t len = arr_len(a); ...}char arr[16] = {0};fun(arr);

虽然 fun() 函数的参数被写成数组形式(char a[]),但是如果读者看过我之前的文章,应该会明白在 fun() 函数内部,a 其实是会退化成指针的。因此 fun() 函数内部的 arr_len 宏计算 arr 长度时,其实计算的是指针的长度,而不是数组的长度,这就极可能引发 bug,并且这个 bug 会隐藏的比较深,难以发现。

这两个C语言宏有人说没用,其实作用很大,也许是他们不知道而已

极可能引发 bug

出现这样的问题,是因为 arr_len 宏不能检查传递给自己的是否数组。那有没有办法确保传递给 arr_len 宏的一定是数组呢?自然是有的,结合__builtin_types_compatible_p() 函数和 BUILD_BUG_ON_ZERO 宏就能轻易实现,下面是一个C语言代码示例,请看:

#define must_be_array(a) \ BUILD_BUG_ON_ZERO(__builtin_types_compatible_p(typeof(a), typeof(&a[0])))

这两个C语言宏有人说没用,其实作用很大,也许是他们不知道而已

C语言代码示例

在上述C语言代码中,表达式__builtin_types_compatible_p(typeof(a), typeof(&a[0]))可以判断 a 是否数组,如果 a 是数组,那么 a 的数据类型与 &a[0] 的数据类型不相同,表达式__builtin_t... 返回 0,按照上一篇文章的分析,BUILD_BUG_ON_ZERO(0)是合法的,可以通过编译。

如果 a 是指针,那么 a 的数据类型与 &a[0] 的数据类型相同,表达式__builtin_t... 返回 1,BUILD_BUG_ON_ZERO(1)是非法的,在编译阶段就会报错

must_be_array(a) 宏能够确保 a 一定是数组,否则就会报错,这就为C语言程序提供了安全检查,并且这个检查是在编译时进行的,能够帮助程序员在程序开发阶段发现错误。

帮助C语言程序员在程序开发阶段发现错误

现在对 arr_len 宏做修改,相关C语言代码如下,请看:

#define arr_len(arr) \({ \ must_be_array(arr); \ (size_t)(sizeof(arr)/sizeof(*arr)); \})

这两个C语言宏有人说没用,其实作用很大,也许是他们不知道而已

对 arr_len 宏做修改

编写相关C语言代码测试之,如下:

#include <stdio.h>#define BUILD_BUG_ON_ZERO(e) (sizeof(struct { int:-!!(e); }))#define BUILD_BUG_ON_NULL(e) ((void *)sizeof(struct { int:-!!(e); }))#define must_be_array(a) \ BUILD_BUG_ON_ZERO(__builtin_types_compatible_p(typeof(a), typeof(&a[0])))#define arr_len(arr) \({ \ must_be_array(arr); \ (int)(sizeof(arr)/sizeof(*arr)); \})int main(){ int arr1[16] = {0}; int *arr2 = NULL; printf('%d %d\n', arr_len(arr1), arr_len(arr2)); return 0;}

这两个C语言宏有人说没用,其实作用很大,也许是他们不知道而已

测试C语言代码

编译这段C语言代码,发现编译器报错了:

这两个C语言宏有人说没用,其实作用很大,也许是他们不知道而已

编译器报错

根据错误信息,很容易确定是 arr2 引发的编译错误。这其实就是 must_be_array 宏的作用了,它发现传递给 arr_len 的不是数组,就会报错。现在将 arr2 也改为数组:

int main(){ int arr1[16] = {0}; int *arr2[32] = {0}; printf('%d %d\n', arr_len(arr1), arr_len(arr2)); return 0;}

这两个C语言宏有人说没用,其实作用很大,也许是他们不知道而已

将 arr2 也改为数组

编译修改后的C语言代码,发现没有错误了,执行之,得到如下输出:

# gcc t2.c# ./a.out 16 32

可见,arr_len现在安全多了,它能够计算数组长度(包括指针数组),也能够判断传递给自己的究竟是指针还是数组。

小结

本节讨论了上一节介绍的两个宏的实用实例,并给出了一个具体的C语言程序示例,可见,即使自定义的编译时asset只能检查常数表达式,也是用途极大的,很能够帮助C语言程序员开发出更安全的程序。有读者指出,C11 已经原生支持 static_assert(未考证),类似于 BUILD_BUG_ON_XX 的宏已经没有必要存在了。但是遗憾的是,相当多的嵌入式设备并不支持 C11。

另外,我的这两篇文章并不仅仅是讨论 BUILD_BUG_ON_XX 宏,我更希望是向初学者介绍C语言程序开发中的灵活思想。

点个赞再走吧

欢迎在评论区一起讨论,质疑。文章都是手打原创,每天最浅显的介绍C语言、linux等嵌入式开发,喜欢我的文章就关注一波吧,可以看到最新更新和之前的文章哦。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多