ChatGPT解决这个技术问题 Extra ChatGPT

拒绝指针的数组大小的宏

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

#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)) 这样的宏可能很有用,以避免必须同时指定数组的名称和给出数组大小的常量的名称。

o
ouah

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 内核开发人员会解决这个问题。
D
David Ranieri

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 - 谢谢 - 是的,这是一个很好的额外测试
一个数组 :) 数组的地址将与数组的值相同,因为该值衰减为指向第一个元素的指针。
b
bluss

使用 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(&(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(&(arr), T(*)[sizeof(arr)/sizeof(arr[0])]: sizeof(arr)/sizeof(arr[0])) 这会创建一个指向指定类型数组的数组指针。如果传递的参数不是正确类型或大小的数组,则会出现编译器错误。 100% 便携标准 C.
4
4566976

使用 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= 选项。
C
Community

这是使用名为 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 扩展的平台。在这些情况下,我建议只使用标准宏,而不必担心意外传入指向宏的指针。


D
Digital Trauma

这是另一个依赖于 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 - 是的,感谢澄清 - 这是有道理的 - 我会更新答案,因为这显然是一个改进。
n
not-a-user

我个人最喜欢的,尝试过 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"

小问题:对于 const 指针后面的数组,__builtin_types_compatible_p 版本失败(因为 const 和非 const 类型不匹配)
h
hurufu

该系列的另一个例子。

#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)) 并且根本不使用任何临时变量;)
M
Michael M.

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

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