5

C语言程序编译时assert

 3 years ago
source link: https://blog.popkx.com/c%E8%AF%AD%E8%A8%80%E7%A8%8B%E5%BA%8F%E7%BC%96%E8%AF%91%E6%97%B6assert/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

今天翻看 Linux 内核源代码时,发现两行非常有意思的代码,如下:

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

37f9c4b7ae62957a9e2dd56910e72a0a.png

这两行C语言代码有什么含义呢?

要理解这两行C语言代码,关键就是理解 int:-!!(e) ,但是“:-!!”符号看起来很陌生,C语言中似乎并没有这样的符号。其实不是的,“:-!!”这几个符号都是C语言中的基本符号组成的。

首先,不应该将“:”与 int 剥离,所以 int:-!!(e) 应该这么看,int: (-!!(e)),这就清楚了,显然是位域(bitfield)的定义方法,其中 -!!(e) 是位域的长度。

对于 -!!(e),应该将 e 看作是一个条件表达式,此时 !! 符号可以将其转换为布尔值(即0或者1,读者自己思考原因)。在C语言中,非零即可认为是真,因此 2,3,88 等都看看作真。在本例中,定义位域时,长度不应该超过 int 的宽度,所以如果没有 !! 符号,BUILD_BUG_ON_XX 宏的适用范围就很小了。

现在明白了,!!(e) 的值要么是 0,要么是 1。再考虑前面的负号,-!!(e) 要么是 0,要么是 -1,即对于 int:-!!(e) 来说,只有两种情况:

int: 0
// 或者
int: -1

显然,位域的长度不能是负数,所以如果表达式 e 为真时,宏 BUILD_BUG_ON_XX 就是非法的了,在编译阶段就会报错

“编译时”和“运行时”

从某种程度上来看,上述C语言宏可以看作是编译时的 assert()。有读者可能会问,既然如此,为什么不直接使用 assert(),而是花大力气自定义呢?

读者应该注意“编译时”这个关键词,BUILD_BUG_ON_XX 宏在编译阶段就可以检查错误,这就能确保程序员能够在程序运行之前发现错误,并修改相关的C语言代码。与之对应的, assert() 只能在程序运行时检查错误,程序运行时出错就麻烦了,至少需要程序员编写相应的错误处理逻辑C语言代码。

如果能够在程序开发阶段发现错误,是多么美好的一件事啊。

那 assert() 就没有存在的必要了?暂时还不是,对于 BUILD_BUG_ON_XX 宏中的条件表达式 e,目前的C语言语法只支持常量表达式,对于变量表达式就无能为力了,只能使用 assert(),例如:

int a = 1;

BUILD_BUG_ON_ZERO(1<0);     // 合法
BUILD_BUG_ON_ZERO(a<0);     // 非法

assert(a<0);    // 合法

读者可能会问,BUILD_BUG_ON_XX 宏只能判断常量表达式,那它还有什么应用价值呢?毕竟两个常量的对比谁会弄错呢?BUILD_BUG_ON_XX 宏当然有应用价值,而且还挺好用,下一节将结合实例讨论,敬请关注。

事实上,这种借助C语言语法的实现编译时判断的技巧有很多种,例如:

5d3ba60d84496baaa1d71a9a7dc3fac0.png

它们的原理和作用都是类似的,留给读者自己分析了,这里不再赘述。

BUILD_BUG_ON_XX 宏只能判断常量表达式,似乎没有什么应用价值,毕竟两个常量的对比谁会弄错呢?其实 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() 函数返回值是常量。

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

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

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

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

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

出现这个问题是因为 arr_len 宏不能检查传递给自己的是否数组。

此时 arr_len() 宏就会得出错误值,造成程序的未知错误


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK