经常教授的标准数组大小宏是
#define ARRAYSIZE(arr) (sizeof(arr) / sizeof(arr[0]))
或一些等效的形式。然而,当指针被传入时,这种事情会默默地成功,并给出在运行时看似合理的结果,直到事情神秘地分崩离析。
犯这个错误太容易了:重构了一个具有局部数组变量的函数,将一些数组操作移动到一个以数组作为参数调用的新函数中。
所以,问题是:是否有一个“卫生”宏来检测 C 中 ARRAYSIZE
宏的滥用,最好是在编译时?在 C++ 中,我们只使用专门用于数组参数的模板;在 C 语言中,我们似乎需要一些方法来区分数组和指针。 (例如,如果我想拒绝数组,我只会做例如 (arr=arr, ...)
因为数组分配是非法的)。
char a[MAGIC_STUFF(COMPLICATED(X, Z+FOO(G)))];
绝对没有错,并且不想在下面再次输入它。如果信息在那里并且工具集在那里,请使用它。
#define SEND_FIXED_COMMAND(cmd) send_command((arr), sizeof (arr))
这样的宏可能很有用,以避免必须同时指定数组的名称和给出数组大小的常量的名称。
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
中的位域宽度需要整数常量表达式。
当 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
的地址)
IS_INDEXABLE(arg)
需要什么?据我所知,这总是返回非零
error: subscripted value is neither array nor pointer nor vector
使用 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
并使用指针静态失败。
这是一个不完美的答案,但也许读者可以改进它并摆脱该类型参数!
_Generic(&(A), T(*)[sizeof(A) / sizeof((A)[0])]: sizeof(A) / sizeof((A)[0]))
这使得第二次测试变得不必要,我认为错误消息 error: '_Generic' selector of type 'int **' is not compatible with any association
比 error: invalid use of void expression
更容易理解。可悲的是,我仍然不知道如何摆脱该类型参数。 :-(
#define ARRAYSIZE(arr, T) _Generic(&(arr), T(*)[sizeof(arr)/sizeof(arr[0])]: sizeof(arr)/sizeof(arr[0]))
这会创建一个指向指定类型数组的数组指针。如果传递的参数不是正确类型或大小的数组,则会出现编译器错误。 100% 便携标准 C.
使用 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=
选项。
这是使用名为 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 扩展的平台。在这些情况下,我建议只使用标准宏,而不必担心意外传入指向宏的指针。
这是另一个依赖于 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')
= {};
效果会更好:如果你传递一个指针,你会得到“空标量初始化器”。这使其可移植到例如结构数组。
= {};
对我不起作用:( - 如果我传递一个简单的 int 数组,那么我也会得到“错误:空标量初始化程序”。但我可以传递整数数组、指针数组或结构数组到 = 0;
版本没有困难。
{[0] = 0}
版本确实会产生一些关于缺少大括号的警告。
#define ARRAYSIZE(arr) ({typeof(arr) arr##_is_pointer = {}; sizeof(arr)/sizeof(arr[0]);})
。没有指定的初始化程序。这适用于 int 数组和 struct 数组,没有警告。
我个人最喜欢的,尝试过 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 类型不匹配)
该系列的另一个例子。
#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 和(如果你喜欢的话)条件它依赖于 C99 VLA 特性
({ ... })
符号创建了新范围。唯一的问题是当您像这样使用它时:double length[234]; const size_t size = LENGTHOF(length);
。而且您始终可以复制 (sizeof X / (sizeof X[0] ?: 1))
并且根本不使用任何临时变量;)
太糟糕了,是的,但这很有效,而且它是便携的。
#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 <==
int arr2[2];
对我来说失败了。在这种情况下 sizeof(arr)
和 sizeof(&arr[0])
c 都等于 8
sizeof(arr) != sizeof(&arr[0])
是一个糟糕的测试。 1) 它具有误导性:乍一看,人们可能会认为 sizeof(&arr[0])
在某种程度上依赖于 arr
,而实际上它几乎从不依赖。在我所知道的所有平台上,它都等同于 sizeof(void*)
。 (您碰巧知道sizeof(int*)!=sizeof(void*)
所在的平台吗?) 2) 正如@DigitalTrauma 所指出的,此错误检查很容易导致误报。为什么不使用 (((void *) &arg) == ((void *) arg))
?如果你想改变它,我可以投票 - 运行时错误消息至少对于调试构建非常有用。
不定期副业成功案例分享