这段代码如何在不使用 sizeof( ) 的情况下确定数组大小?

How does this piece of code determine array size without using sizeof( )?

通过一些 C 面试问题,我发现了一个说明 "How to find the size of an array in C without using the sizeof operator?" 的问题,其解决方案如下。它有效,但我不明白为什么。

#include <stdio.h>

int main() {
    int a[] = {100, 200, 300, 400, 500};
    int size = 0;

    size = *(&a + 1) - a;
    printf("%d\n", size);

    return 0;
}

不出所料,returns 5.

编辑:人们指出 this 答案,但语法确实有点不同,即索引方法

size = (&arr)[1] - arr;

所以我相信这两个问题都是有效的,并且解决问题的方法略有不同。感谢大家的大力帮助和详尽的解释!

这一行最重要:

size = *(&a + 1) - a;

如您所见,它首先获取 a 的地址并对其加一。然后,它取消引用该指针并从中减去 a 的原始值。

C 中的指针运算导致 return 数组中的元素数,或 5。和&a相加是指向a后5ints的下一个数组的指针。之后,此代码取消引用结果指针并从中减去 a(已衰减为指针的数组类型),给出数组中元素的数量。

关于指针算法如何工作的详细信息:

假设您有一个指向 int 类型并包含值 (int *)160 的指针 xyz。当您从 xyz 中减去任何数字时,C 指定从 xyz 中减去的实际数量是该数字乘以它指向的类型的大小。例如,如果您从 xyz 中减去 5,如果指针算法不适用,则结果 xyz 的值将是 xyz - (sizeof(*xyz) * 5)

由于a5 int 类型的数组,因此结果值为5。但是,这不适用于指针,只能用于数组。如果你用一个指针试试这个,结果总是 1.

这里有一个小例子,显示了地址以及它是如何未定义的。 left-hand 方显示地址:

a + 0 | [a[0]] | &a points to this
a + 1 | [a[1]]
a + 2 | [a[2]]
a + 3 | [a[3]]
a + 4 | [a[4]] | end of array
a + 5 | [a[5]] | &a+1 points to this; accessing past array when dereferenced

这意味着代码从 &a[5](或 a+5)中减去 a,得到 5.

请注意,这是未定义的行为,在任何情况下都不应使用。不要期望它的行为在所有平台上都是一致的,也不要在生产程序中使用它。

嗯,我怀疑这在早期的 C 语言中是行不通的。不过它很聪明。

一步一个脚印:

  • &a 获取指向 int[5] 类型对象的指针
  • +1 获取下一个这样的对象假设有一个数组
  • * 有效地将地址转换为指向 int
  • 的类型指针
  • -a 减去两个 int 指针,return计算它们之间的 int 实例数。

我不确定它是否完全合法(在此我的意思是 language-lawyer 合法 - 它在实践中不会起作用),因为某些类型操作正在进行。例如,当两个指针指向同一个数组中的元素时,您只需 "allowed" 减去两个指针。 *(&a+1) 是通过访问另一个数组合成的,尽管它是父数组,因此实际上并不是指向与 a 相同数组的指针。 此外,虽然您可以合成一个超过数组最后一个元素的指针,并且您可以将任何对象视为 1 个元素的数组,但取消引用 (*) 的操作不是 "allowed"这个合成指针,即使它在这种情况下没有任何行为!

我怀疑在 C 的早期(K&R 语法,有人知道吗?),数组更快地变成指针,所以 *(&a+1) 可能只是 return 的地址int** 类型的下一个指针。现代 C++ 更严格的定义肯定允许指向数组类型的指针存在并知道数组大小,并且 C 标准可能已经效仿。所有 C 函数代码仅将指针作为参数,因此技术上的可见差异很小。不过我这里只是猜测。

这种详细的合法性问题通常适用于C解释器或lint类型的工具,而不是编译后的代码。解释器可能将二维数组实现为指向数组的指针数组,因为要实现的运行时功能较少,在这种情况下取​​消引用 +1 将是致命的,即使它有效也会给出错误的答案。

另一个可能的弱点是 C 编译器可能对齐外部数组。想象一下,如果这是一个包含 5 个字符的数组 (char arr[5]),当程序执行 &a+1 时,它会调用 "array of array" 行为。编译器可能会决定将 5 个字符的数组 (char arr[][5]) 实际上生成为 8 个字符的数组 (char arr[][8]),以便外部数组很好地对齐。我们正在讨论的代码现在会将数组大小报告为 8,而不是 5。我并不是说特定的编译器肯定会这样做,但它可能会这样做。

当您将 1 加到指针时,结果是 pointed-to 类型的对象序列(即数组)中下一个对象的位置。如果 p 指向一个 int 对象,那么 p + 1 将指向序列中的下一个 int。如果 p 指向 int 的 5 元素数组(在本例中,表达式 &a),则 p + 1 将指向下一个 5 - 序列中 int 的元素数组。

两个指针相减(前提是它们都指向同一个数组对象,或者一个指针指向数组的最后一个元素)得到这两个指针之间的对象(数组元素)的数量。

表达式 &a 产生 a 的地址,类型为 int (*)[5](指向 int 的 5 元素数组的指针)。表达式 &a + 1 生成 a 之后 int 的下一个 5 元素数组的地址,并且类型也为 int (*)[5]。表达式 *(&a + 1) 取消引用 &a + 1 的结果,因此它产生 a 最后一个元素之后的第一个 int 的地址,类型为 int [5] ,在此上下文中“衰减”为 int *.

类型的表达式

类似地,表达式 a“衰减”为指向数组第一个元素的指针,类型为 int *.

图片可能有帮助:

int [5]  int (*)[5]     int      int *

+---+                   +---+
|   | <- &a             |   | <- a
| - |                   +---+
|   |                   |   | <- a + 1
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
+---+                   +---+
|   | <- &a + 1         |   | <- *(&a + 1)
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
+---+                   +---+

这是同一存储的两个视图 - 在左侧,我们将其视为 int 的 5 元素数组序列,而在右侧,我们将其视为int 的序列。我还展示了各种表达式及其类型。

注意,表达式 *(&a + 1) 导致 未定义的行为:

...
If the result points one past the last element of the array object, it shall not be used as the operand of a unary * operator that is evaluated.

C 2011 Online Draft, 6.5.6/9