C语言程序开发中,怎样检查接收到的参数是指针还是数组?

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

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


如果读者看了上一篇文章,应该已经明白这两个宏具备编译时检查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 会隐藏的比较深,难以发现。

出现这样的问题,是因为 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语言代码中,表达式__builtin_types_compatible_p(typeof(a), typeof(&a[0]))可以判断 a 是否数组,如果 a 是数组,那么 a 的数据类型与 &a[0] 的数据类型不相同,表达式__builtin_types_compatible_p(typeof(a), typeof(&a[0]))返回 0,按照上一篇文章的分析,BUILD_BUG_ON_ZERO(0)是合法的,可以通过编译。

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

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

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

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


编写相关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语言代码,发现编译器报错了:

根据错误信息,很容易确定是 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语言代码,发现没有错误了,执行之,得到如下输出:

# 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语言
添加新评论

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