可靠地确定数组中元素的数量

16

每个C程序员都可以使用这个众所周知的宏来确定数组中元素的数量:

#define NUM_ELEMS(a) (sizeof(a)/sizeof 0[a])

这是一个典型的使用案例:

int numbers[] = {2, 3, 5, 7, 11, 13, 17, 19};
printf("%lu\n", NUM_ELEMS(numbers));          // 8, as expected

然而,没有任何东西可以防止程序员意外地传递指针而不是数组:

int * pointer = numbers;
printf("%lu\n", NUM_ELEMS(pointer));

在我的系统上,这将打印2,因为显然指针的大小是整数的两倍。我思考如何防止程序员错误地传递指针,我找到了一个解决方案:

#define NUM_ELEMS(a) (assert((void*)&(a) == (void*)(a)), (sizeof(a)/sizeof 0[a]))

这是因为指向数组的指针和指向其第一个元素的指针具有相同的值。如果你传递一个指针,那么这个指针将会与指向它自身的指针进行比较,几乎总是返回false。(唯一的例外是递归的void指针,也就是指向自己的void指针。我可以接受这种情况。)

不小心传递指针而不是数组现在会在运行时触发错误:

Assertion `(void*)&(pointer) == (void*)(pointer)' failed.

不错!现在我有几个问题:

  1. 在标准C中,我将assert用作逗号表达式的左操作数,这种用法是否有效?也就是说,标准是否允许我将assert用作表达式?如果这是一个愚蠢的问题,请原谅我 :)

  2. 是否可以在编译时进行检查?

  3. 我的C编译器认为int b[NUM_ELEMS(a)];是VLA。有没有什么方法说服它相反呢?

  4. 我是第一个想到这个问题的人吗?如果是,有多少处女可以等着我在天堂里?:)


1
关于第四部分,我很确定它不是72。我认为那是保留给其他东西的值... - im so confused
1
你是不是指的是 sizeof a[0] - Some programmer dude
4
实际上,他很简洁...要切换它们,你需要使用(a)[0]来避免宏参数可能的误解析...不过*(a)也可以正常工作。 - Jim Balter
2
@FredOverflow,请查看我的答案更新,提供了一种新的解决方案。 - ouah
1
@a[0] == *(a + 0) == *(0 + a) == 0[a]:@JoachimPileborg - Frerich Raabe
显示剩余7条评论
2个回答

10

我在使用assert函数作为逗号表达式的左操作数时,是否符合C语言标准?也就是说,标准是否允许我将assert函数用作表达式?

是的,这是有效的,因为逗号运算符的左操作数可以是void类型的表达式,而assert函数的返回类型是void

我的C编译器认为int b[NUM_ELEMS(a)];是VLA(变长数组)。有什么方法可以让它改变看法吗?

它认为如此是因为逗号表达式的结果从未是常量表达式(例如,1,2不是常量表达式)。

编辑1:添加以下更新。

我有另一种版本的您的宏,它可以在编译时工作:

#define NUM_ELEMS(arr)                                                 \
 (sizeof (struct {int not_an_array:((void*)&(arr) == &(arr)[0]);}) * 0 \
  + sizeof (arr) / sizeof (*(arr)))

这个方法似乎可以用来初始化具有静态存储期的对象,并且它也可以正确地处理您的int b[NUM_ELEMS(a)]示例。

编辑2:

为了回应@DanielFischer的评论。以上宏仅适用于gcc 不使用 -pedantic,因为gcc会接受:

(void *) &arr == arr
作为整数常量表达式,虽然它考虑
(void *) &ptr == ptr

不是整数常量表达式。 根据C语言,它们都不是整数常量表达式,并且使用-pedanticgcc在这两种情况下都正确发出诊断。

据我所知,没有100%可移植的方法来编写此 NUM_ELEM 宏。 C具有更灵活的初始化程序常量表达式规则(请参见C99中的6.6p7),可以利用这些规则编写此宏(例如使用sizeof和复合字面量)但是在块范围内,C不需要初始化器为常量表达式,因此无法创建适用于所有情况的单个宏。

EDIT3:

值得一提的是,Linux内核具有一个ARRAY_SIZE宏(位于include/linux/kernel.h中),当执行稀疏(内核静态分析检查程序)时实施了这样的检查。

他们的解决方案不可移植并且使用了两个GNU扩展:

  • typeof操作符
  • __builtin_types_compatible_p内建函数

基本上看起来像这样:

#define NUM_ELEMS(arr)  \
 (sizeof(struct {int :-!!(__builtin_types_compatible_p(typeof(arr), typeof(&(arr)[0])));})  \
  + sizeof (arr) / sizeof (*(arr)))

不幸的是,使用-pedantic-errors,我从gcc得到了error: bit-field ‘not_an_array’ width not an integer constant expression [-pedantic]的错误提示,clang默认情况下也会给出一个错误提示 :( - Daniel Fischer
@DanielFischer 请看我的第二次编辑,针对您的评论进行了修改。 - ouah
喜欢你的解决方案;将其移植到IAR C99时,我发现了一个编译器意外:“Error[Pe060]: this operator is not allowed in an integral constant expression”;“this operator”是sizeof,而以下内容不会产生错误(也不会产生所需的结果):“#define ELEMENTS_NUM(arr) (sizeof (struct {int i:(1 == true);}) * 0 + sizeof (arr) / sizeof (*(arr)))”;有什么想法吗? - lkanab

4
  1. 是的。逗号运算符的左表达式始终被评估为void表达式(C99 6.5.17#2)。由于assert()是一个void表达式,所以一开始就没有问题。
  2. 可能。虽然C预处理器不知道类型和强制转换,也无法比较地址,但您可以使用与在编译时评估sizeof()相同的技巧,例如声明一个数组,其维度是布尔表达式。当为0时,它是一个约束违规,必须发出诊断。我在这里尝试了一下,但到目前为止还没有成功...也许答案实际上是“否”。
  3. 不。指针类型的转换不是整数常量表达式。
  4. 可能不是(这些天没有新东西了)。数量不定的性别不明的处女 :-)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接