拒绝指针的数组大小的宏

Posted

技术标签:

【中文标题】拒绝指针的数组大小的宏【英文标题】:Array-size macro that rejects pointers 【发布时间】:2013-10-27 11:49:20 【问题描述】:

经常教授的标准数组大小宏是

#define ARRAYSIZE(arr) (sizeof(arr) / sizeof(arr[0]))

或一些等效的形式。然而,当指针被传入时,这种事情会默默地成功,并给出在运行时看似合理的结果,直到事情神秘地分崩离析。

这个错误太容易犯了:重构一个具有局部数组变量的函数,将一些数组操作移到一个以数组作为参数调用的新函数中。

所以,问题是:是否有一个“卫生”宏来检测 C 中 ARRAYSIZE 宏的滥用,最好是在编译时?在 C++ 中,我们只使用专门用于数组参数的模板;在 C 语言中,我们似乎需要一些方法来区分数组和指针。 (例如,如果我想拒绝数组,我会这样做,例如 (arr=arr, ...),因为数组分配是非法的)。

【问题讨论】:

这会很粗糙,因为数组在几乎所有上下文中都会衰减为指针。 为什么会有人需要这样的宏?这仅适用于在代码中由固定大小定义的数组,为什么您需要计算您知道自己编写的内容?如果答案是“也许您在代码的另一部分并且您不再拥有此信息”,那么我接下来的问题是:如果数组不衰减到指针,这怎么可能? - 为实现这一点而设计的一段代码? @Eregrith 通过扩展,这种观点也可能是“为什么有人需要任何类型的编译时计算或元编程”? “你知道你写了什么”的想法既荒谬又没用。没有法律规定您必须首先手写。 @Eregrith 我认为写char a[MAGIC_STUFF(COMPLICATED(X, Z+FOO(G)))]; 绝对没有任何问题,并且不想在下面再次输入。如果信息在那里并且工具集在那里,请使用它。 @Eregrith:我想到了至少两种情况:(1)数组大小可能没有指定,但可以从初始化列表中推断出来; (2) 有一个像#define SEND_FIXED_COMMAND(cmd) send_command((arr), sizeof (arr)) 这样的宏可能很有用,以避免必须同时指定数组的名称和给出数组大小的常量的名称。 【参考方案1】:

Linux 内核使用ARRAY_SIZE 的一个很好的实现来处理这个问题:

#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]) + __must_be_array(arr))

#define __must_be_array(a) BUILD_BUG_ON_ZERO(__same_type((a), &(a)[0]))

#define __same_type(a, b) __builtin_types_compatible_p(typeof(a), typeof(b))

当然,这仅在 GNU C 中是可移植的,因为它使用了两个内在函数: typeof 运算符和 __builtin_types_compatible_p 函数。它还使用他们的“著名”BUILD_BUG_ON_ZERO 宏,该宏仅在 GNU C 中有效。

假设编译时评估要求(这是我们想要的),我不知道这个宏的任何可移植实现。

“半便携式”实现(并不涵盖所有情况)是:

#define ARRAY_SIZE(arr)  \
    (sizeof(arr) / sizeof((arr)[0]) + STATIC_EXP(IS_ARRAY(arr)))

#define IS_ARRAY(arr)  ((void*)&(arr) == &(arr)[0])
#define STATIC_EXP(e)  \
    (0 * sizeof (struct  int ARRAY_SIZE_FAILED:(2 * (e) - 1);))

使用gcc,如果参数是-std=c99 -Wall 中的数组,则不会发出警告,但-pedantic 会发出警告。原因是IS_ARRAY 表达式不是整型常量表达式(整型常量表达式中不允许转换为指针类型和下标运算符),STATIC_EXP 中的位域宽度需要整型常量表达式。

【讨论】:

哦,太好了,这是一块宝石。我应该认为 Linux 内核开发人员会解决这个问题。【参考方案2】:

arr 是指针时,此版本的ARRAYSIZE() 返回0,当它是纯数组时返回大小

#include <stdio.h>

#define IS_INDEXABLE(arg) (sizeof(arg[0]))
#define IS_ARRAY(arg) (IS_INDEXABLE(arg) && (((void *) &arg) == ((void *) arg)))
#define ARRAYSIZE(arr) (IS_ARRAY(arr) ? (sizeof(arr) / sizeof(arr[0])) : 0)

int main(void)

    int a[5];
    int *b = a;
    int n = 10;
    int c[n]; /* a VLA */

    printf("%zu\n", ARRAYSIZE(a));
    printf("%zu\n", ARRAYSIZE(b));
    printf("%zu\n", ARRAYSIZE(c));
    return 0;

输出:

5
0
10

正如 Ben Jackson 所指出的,您可以强制运行时异常(除以 0)

#define IS_INDEXABLE(arg) (sizeof(arg[0]))
#define IS_ARRAY(arg) (IS_INDEXABLE(arg) && (((void *) &arg) == ((void *) arg)))
#define ARRAYSIZE(arr) (sizeof(arr) / (IS_ARRAY(arr) ? sizeof(arr[0]) : 0))

遗憾的是,您不能强制出现编译时错误(arg 的地址必须在运行时进行比较)

【讨论】:

如果在不好的情况下会出现编译时错误(除以 0?)会更好。 IS_INDEXABLE(arg)需要什么?据我所知,这总是返回非零 @DigitalTrauma,因为当参数不是数组(或指针)时会引发错误。 error: subscripted value is neither array nor pointer nor vector @AlterMann - 谢谢 - 是的,这是一个很好的额外测试 一个数组 :) 数组的地址将与数组的值相同,因为值衰减为指向第一个元素的指针。【参考方案3】:

使用 C11,我们可以使用 _Generic 区分数组和指针,但我只有在提供元素类型的情况下才找到一种方法:

#define ARRAY_SIZE(A, T) \
    _Generic(&(A), \
            T **: (void)0, \
            default: _Generic(&(A)[0], T *: sizeof(A) / sizeof((A)[0])))


int a[2];
printf("%zu\n", ARRAY_SIZE(a, int));

宏检查: 1) 指向 A 的指针不是指向指针的指针。 2) 指向元素的指针是指向 T 的指针。它的计算结果为 (void)0 并使用指针静态失败。

这是一个不完美的答案,但也许读者可以改进它并摆脱那个类型参数!

【讨论】:

与其检查“指向A的指针不是指向指针的指针”,不如直接检查指向A的指针是否指向数组? _Generic(&amp;(A), T(*)[sizeof(A) / sizeof((A)[0])]: sizeof(A) / sizeof((A)[0])) 这使得第二次测试变得不必要了,我认为错误消息error: '_Generic' selector of type 'int **' is not compatible with any associationerror: invalid use of void expression 更容易理解。可悲的是,我仍然不知道如何摆脱该类型参数。 :-( 如果可以传递元素类型,那其实还是很简单的。 #define ARRAYSIZE(arr, T) _Generic(&amp;(arr), T(*)[sizeof(arr)/sizeof(arr[0])]: sizeof(arr)/sizeof(arr[0])) 这会创建一个指向指定类型数组的数组指针。如果传递的参数不是正确类型或大小的数组,则会出现编译器错误。 100% 便携式标准 C.【参考方案4】:

使用 typeof 而不是类型参数修改 bluss 的答案:

#define ARRAY_SIZE(A) \
    _Generic(&(A), \
    typeof((A)[0]) **: (void)0, \
    default: sizeof(A) / sizeof((A)[0]))

【讨论】:

typeof 是 GCC 扩展,因此此代码仅适用于 GCC。如果您特定于 GCC,那么您可以更好地使用基于 __builtin_types_compatible_p 或类似的东西 - 在任何该代码可以工作的情况下都可以使用,但它也可以在旧版本的 GCC 或指定的旧标准中工作通过-std= 选项。【参考方案5】:

这是使用名为 statement expressions 的 GNU 扩展的一种可能解决方案:

#define ARRAYSIZE(arr) \
    (typedef char ARRAYSIZE_CANT_BE_USED_ON_POINTERS[sizeof(arr) == sizeof(void*) ? -1 : 1]; \
     sizeof(arr) / sizeof((arr)[0]);)

这使用static assertion 来断言sizeof(arr) != sizeof(void*)。这有一个明显的限制——你不能在大小恰好是一个指针的数组上使用这个宏(例如,指针/整数的 1 长度数组,或者 32 位上的 4 长度字节数组平台)。但是这些特殊情况可以很容易地解决。

此解决方案不可移植到不支持此 GNU 扩展的平台。在这些情况下,我建议只使用标准宏,而不必担心意外传入指向宏的指针。

【讨论】:

【参考方案6】:

这是另一个依赖 gcc typeof extension:

#define ARRAYSIZE(arr) (typeof (arr) arr ## _is_a_pointer __attribute__((unused)) = ; \
                         sizeof(arr) / sizeof(arr[0]);)

这通过尝试设置相同的对象并使用数组指定的初始化程序对其进行初始化来工作。如果传递了一个数组,那么编译器很高兴。如果指针被传递,编译器会报错:

arraysize.c: In function 'main':
arraysize.c:11: error: array index in non-array initializer
arraysize.c:11: error: (near initialization for 'p_is_a_pointer')

【讨论】:

这很好!实际上,如果你使用= ; 效果会更好:如果你传递一个指针,你会得到“空标量初始值设定项”。这使其可移植到例如结构数组。 @nneonneo - = ; 对我不起作用 :( - 如果我传递一个简单的 int 数组,那么我也会得到“错误:空标量初始化程序”。但我可以传递整数数组, 指向= 0; 版本的指针数组或结构数组。 [0] = 0 版本确实会产生一些警告,但是,如果您有数组数组或结构数组,则会丢失大括号。 @DigitalTrauma:抱歉,我可能有点困惑。代码是#define ARRAYSIZE(arr) (typeof(arr) arr##_is_pointer = ; sizeof(arr)/sizeof(arr[0]);)。没有指定的初始化程序。这适用于 int 数组和 struct 数组,没有警告。 @nneonneo - 是的,感谢您的澄清 - 这是有道理的 - 我会更新答案,因为这显然是一种改进。【参考方案7】:

很糟糕,是的,但它很有效,而且很便携。

#define ARRAYSIZE(arr) ((sizeof(arr) != sizeof(&arr[0])) ? \
                       (sizeof(arr)/sizeof(*arr)) : \
                       -1+0*fprintf(stderr, "\n\n** pointer in ARRAYSIZE at line %d !! **\n\n", __LINE__))

这不会在编译时检测到任何东西,但会在stderr 中打印出一条错误消息,如果它是一个指针如果数组长度为1,则返回-1

==> DEMO

【讨论】:

在我的 64 位机器上使用 int arr2[2]; 对我来说失败了。在这种情况下,sizeof(arr)sizeof(&amp;arr[0])c 都等于 8 PRO:在我使用堆上分配的数组的情况下报告问题。在这种情况下,对于任何大小的数组,即使是 google chromium COUNT_OF 宏也会返回 2。 CON:编译时不带有迂腐警告。 sizeof(arr) != sizeof(&amp;arr[0]) 是一个糟糕的测试。 1) 它具有误导性:乍一看,人们可能会认为sizeof(&amp;arr[0]) 在某种程度上依赖于arr,而实际上它几乎从不依赖。在我所知道的所有平台上,它都相当于sizeof(void*)。 (你碰巧知道sizeof(int*)!=sizeof(void*) 所在的平台吗?)   2)正如@DigitalTrauma 所指出的,这种错误检查很容易导致误报。为什么不使用(((void *) &amp;arg) == ((void *) arg))?如果你愿意改变这一点,我可以投票 - 运行时错误消息至少对于调试构建非常有用。【参考方案8】:

我个人最喜欢的,试过 gcc 4.6.3 和 4.9.2:

#define STR_(tokens) # tokens

#define ARRAY_SIZE(array) \
    ( \
        _Static_assert \
        ( \
            ! __builtin_types_compatible_p(typeof(array), typeof(& array[0])), \
            "ARRAY_SIZE: " STR_(array) " [expanded from: " # array "] is not an array" \
        ); \
        sizeof(array) / sizeof((array)[0]); \
    )

/*
 * example
 */

#define not_an_array ((char const *) "not an array")

int main () 
    return ARRAY_SIZE(not_an_array);

编译器打印

x.c:16:12: error: static assertion failed: "ARRAY_SIZE: ((char const *) \"not an array\") [expanded from: not_an_array] is not an array"

【讨论】:

小问题:__builtin_types_compatible_p 版本对于 const 指针后面的数组失败(因为 const 和非 const 类型不匹配)【参考方案9】:

该集合的另一个示例。

#define LENGTHOF(X) ( \
    const size_t length = (sizeof X / (sizeof X[0] ?: 1)); \
    typeof(X[0]) (*should_be_an_array)[length] = &X; \
    length; )

优点:

    它适用于普通数组、可变长度数组、多维数组 数组,零大小结构的数组 如果您传递任何指针、结构或 工会 它不依赖于 C11 的任何功能 它会给你带来非常易读的错误

缺点:

    这取决于一些 gcc 扩展:Typeof, Statement Exprs,和(如果你喜欢的话)Conditionals 这取决于 C99 VLA 功能

【讨论】:

作为一个缺点,这也会创建一个变量“长度”,可能与代码中的另一个变量发生冲突 它不会冲突,因为( ... ) 符号创建了新的范围。唯一的问题是当你像这样使用它时:double length[234]; const size_t size = LENGTHOF(length); 。而且你总是可以复制(sizeof X / (sizeof X[0] ?: 1)) 并且根本不使用任何临时变量;)

以上是关于拒绝指针的数组大小的宏的主要内容,如果未能解决你的问题,请参考以下文章

C 语言二级指针作为输入 ( 二维数组 | 二维数组内存大小计算 | 指针跳转步长问题 )

函数指针数组

从另一个数组的指针和大小初始化数组

数组和指针

C/C++面试高频知识点八股文

C指针:指向一个固定大小的数组