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

通过一些 C 面试问题,我发现了一个问题: “如何在不使用 sizeof 操作符的情况下在 C 中找到数组的大小?”以下解决方案。它起作用了,但我不明白为什么。

#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;
}

正如预期的那样,它返回5。

人们指出 这个的答案,但语法确实有一点不同,即索引方法

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

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

9125 次浏览

这句话最重要:

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

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

C 语言中的指针算法会返回数组中的元素数,即 5。加上1和 &a是指向 a之后的下一个5个 int的数组的指针。然后,这段代码取消引用生成的指针,并从中减去 a(衰减为指针的数组类型) ,给出数组中的元素数。

关于指针运算的详细信息:

假设您有一个指向 int类型并包含值 (int *)160的指针 xyz。当从 xyz中减去任何数字时,C 指定从 xyz中减去的实际数字是它所指向的类型的大小的两倍。例如,如果从 xyz中减去 5,如果没有应用指针算法,那么得到的 xyz值将是 xyz - (sizeof(*xyz) * 5)

由于 a是由 5 int类型组成的数组,因此得到的值将为5。但是,这不能用于指针,只能用于数组。如果使用指针尝试此操作,结果总是 1

下面是一个小例子,显示了这些地址以及这些地址是如何未定义的:

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 指针,返回它们之间的 int 实例计数。

我不确定它是否完全合法(在这里我的意思是语言-律师法律-不会在实践中起作用) ,考虑到一些类型操作正在进行。例如,只允许减去指向同一数组中元素的两个指针。*(&a+1)是通过访问另一个数组(尽管是父数组)合成的,所以实际上并不是指向与 a相同的数组的指针。 此外,虽然你可以合成一个指针过去的最后一个元素的数组,你可以把任何对象作为一个元素的数组,操作解引用(*)是不允许在这个合成的指针,即使它没有行为在这种情况下!

我怀疑在 C (K & R 语法的早期,有人知道吗?)数组衰减成指针的速度要快得多,因此 *(&a+1)可能只返回下一个 int * * 类型指针的地址。现代 C + + 的更严格的定义肯定允许存在指向数组类型的指针并且知道数组的大小,而且可能 C 标准也跟着这样做了。所有 C 函数代码只接受指针作为参数,因此技术上的可见差异是最小的。但我只是猜测。

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

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

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

减去两个指针(假设它们都指向同一个数组对象,或者一个指向数组的最后一个元素后面的一个)会得到这两个指针之间的对象(数组元素)的数量。

表达式 &a产生 a的地址,并具有类型 int (*)[5](指向 int的5元素数组)。表达式 &a + 1产生 a之后的下一个5元素数组 int的地址,并且还具有类型 int (*)[5]。表达式 *(&a + 1)取消引用 &a + 1的结果,从而产生 a的最后一个元素之后的第一个 int的地址,并且具有类型 a2,在此上下文中该类型“衰减”为类型 a3的表达式。

类似地,表达式 a“衰减”到指向数组第一个元素的指针,并且具有 int *类型。

一张图片可能会有所帮助:

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


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

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

请注意,表达式 *(&a + 1)的结果是 未定义行为:

...
如果结果指向数组对象的最后一个元素后面的一个,则 不得用作被计算的一元 * 运算符的操作数。

C 2011在线草稿 ,6.5.6/9