6

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

 3 years ago
source link: https://blog.popkx.com/c%E8%AF%AD%E8%A8%80%E7%A8%8B%E5%BA%8F%E5%BC%80%E5%8F%91%E4%B8%AD-%E6%80%8E%E6%A0%B7%E6%A3%80%E6%9F%A5%E6%8E%A5%E6%94%B6%E5%88%B0%E7%9A%84%E5%8F%82%E6%95%B0%E6%98%AF%E6%8C%87%E9%92%88%E8%BF%98/
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 内核C语言源码中的两个宏:

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

37f9c4b7ae62957a9e2dd56910e72a0a.png
如果读者看了上一篇文章,应该已经明白这两个宏具备编译时检查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])))

18ea6b1f6d48b1f425f768d8588be6c2.png
在上述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)); \
})

212031811b0bb43f2f81d076ba696ee7.png
编写相关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;
}
f500719aa8477391c04455cee82b5c83.png

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

根据错误信息,很容易确定是 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;
}
1177f3ddb0e6f2f3187062747c03b2ef.png

编译修改后的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语言程序开发中的灵活思想。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK