【问题标题】:Reliably determine the number of elements in an array可靠地确定数组中的元素个数
【发布时间】:2012-09-28 20:18:21
【问题描述】:

每个 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. 我是第一个想到这个的吗?如果是这样,我能指望有多少处女在天堂等着我呢? :)

【问题讨论】:

  • 至于第 (4) 部分,很确定它是 not 72。我认为这是为其他东西保留的值...
  • 你不是说sizeof a[0]吗?
  • @dmckee 实际上他很简洁...要切换它们,您需要(a)[0] 以避免可能的宏参数解析错误...尽管*(a) 工作正常。
  • @FredOverflow 使用新解决方案查看我的答案中的更新
  • @JoachimPileborg: a[0] == *(a + 0) == *(0 + a) == 0[a]

标签: c arrays pointers assert sizeof


【解决方案1】:

我使用 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 中),它在执行 sparse(内核静态分析检查器)时实现这样的检查。

他们的解决方案不可移植,并且使用了两个 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给出错误:(
  • @DanielFischer 看到我的第二个编辑解决您的评论
  • 喜欢你的解决方案;将其移植到 IAR C99 后,我发现一个编译器惊喜:“Error[Pe060]: this operator is not allowed in an integer constant expression”; “此运算符”是 sizeof,以下不会产生错误(也不会产生请求的结果):“#define ELEMENTS_NUM(arr) (sizeof (struct {int i:(1 == true);}) * 0 + sizeof (arr) / sizeof (*(arr)))" ;有什么想法吗?
【解决方案2】:
  1. 是的。逗号运算符的左表达式始终被评估为 void 表达式 (C99 6.5.17#2)。由于assert() 是一个空表达式,所以开始没有问题。
  2. 也许吧。虽然 C 预处理器不知道类型和强制转换并且无法比较地址,但您可以使用与在编译时评估 sizeof() 相同的技巧,例如声明一个数组,其维度是一个布尔表达式。当为 0 时,它是违反约束的,必须发出诊断。我在这里尝试过,但到目前为止还没有成功……也许答案实际上是“否”。
  3. 没有。 (指针类型的)强制转换不是整数常量表达式。
  4. 可能不会(这些天在太阳底下没有什么新鲜事)。不定性别的处女的数量不定:-)

【讨论】:

    猜你喜欢
    • 2016-06-21
    • 2023-01-30
    • 1970-01-01
    • 2020-01-17
    • 1970-01-01
    • 1970-01-01
    • 2017-02-10
    • 1970-01-01
    • 2022-11-27
    相关资源
    最近更新 更多