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

每个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])) 

这是有效的,因为指向数组的指针与指向其第一个元素的指针具有相同的值。 如果改为传递指针,指针将与指向自身的指针进行比较,这几乎总是假的。 (唯一的例外是递归的void指针,也就是指向自身的void指针。我可以忍受它。)

意外地传递指针而不是数组现在在运行时触发错误:

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

太好了! 现在我有几个问题:

  1. 我使用assert作为逗号表达式有效标准C的左操作数吗? 也就是说,标准是否允许我使用assert作为表达式? 对不起,如果这是一个愚蠢的问题:)

  2. 可以在编译时以某种方式完成检查吗?

  3. 我的C编译器认为int b[NUM_ELEMS(a)]; 是一个VLA。 有没有办法说服他呢?

  4. 我是第一个想到这个吗? 如果是这样,我可以期待在天堂等待多少处女? 🙂

我使用assert作为逗号表达式有效标准C的左操作数吗? 也就是说,标准是否允许我使用assert作为表达式?

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

我的C编译器认为int b [NUM_ELEMS(a)]; 是一个VLA。 有没有办法说服他呢?

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

EDIT1:添加以下更新。

我有另一个版本的宏在编译时工作:

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

这似乎也适用于具有静态存储持续时间的对象的初始化程序。 并且它也适用于您的int b[NUM_ELEMS(a)]示例

EDIT2:

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