【问题标题】:How does this piece of code determine array size without using sizeof( )?这段代码如何在不使用 sizeof() 的情况下确定数组大小?
【发布时间】:2019-10-02 21:29:06
【问题描述】:

通过一些 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。

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

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

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

【问题讨论】:

标签: c arrays size language-lawyer pointer-arithmetic


【解决方案1】:

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

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

表达式&amp;a 产生a 的地址,并具有int (*)[5] 类型(指向int 的5 元素数组的指针)。表达式&amp;a + 1 产生int 跟在a 之后的下一个5 元素数组的地址,并且还具有int (*)[5] 类型。表达式*(&amp;a + 1) 取消引用&amp;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 元素数组序列,而在右侧,我们将其视为@987654351 的序列@。我还展示了各种表达式及其类型。

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

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

C 2011 Online Draft, 6.5.6/9

【讨论】:

  • 那个“不得使用”的文字是官方的:C 2018 6.5.6 8.
  • @EricPostpischil:你有 2018 年 pre-pub 草案的链接吗(类似于 N1570.pdf)?
  • @JohnBode:This answera link to the Wayback Machine。我在购买的副本中查看了官方标准。
  • 那么如果有人写了size = (int*)(&amp;a + 1) - a;,这段代码就完全有效了吗? :o
  • @Gizmo 他们可能最初并没有这样写,因为那样你必须指定元素类型;原始版本可能被定义为用于不同元素类型的泛型宏。
【解决方案2】:

这一行是最重要的:

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

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

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

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

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

由于a5 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

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

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

【讨论】:

    【解决方案3】:

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

    一步一步来:

    • &amp;a 得到一个指向 int[5] 类型对象的指针
    • +1 得到下一个这样的对象,假设有一个数组
    • * 有效地将该地址转换为指向 int 的类型指针
    • -a 减去两个 int 指针,返回它们之间的 int 实例数。

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

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

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

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

    【讨论】:

    • 很公平。但是由于难以解释的原因,每个人都使用 sizeof()/sizeof() ?
    • 大多数人都这样做。例如,sizeof(array)/sizeof(array[0]) 给出数组中元素的数量。
    • 允许 C 编译器对齐数组,但我不相信它允许在这样做之后更改数组的类型。通过插入填充字节,对齐会更现实地实现。
    • 指针的减法不限于只有两个指针指向同一个数组——也允许指针超过数组末尾的一个。 &amp;a+1 已定义。正如 John Bollinger 所说,*(&amp;a+1) 不是,因为它试图取消引用一个不存在的对象。
    • 编译器无法将char [][5] 实现为char arr[][8]。数组只是其中重复的对象;没有填充。此外,这将打破 C 2018 6.5.3.4 7 中的(非规范)示例 2,它告诉我们可以使用 sizeof array / sizeof array[0] 计算数组中的元素数。
    猜你喜欢
    • 2016-06-25
    • 2017-03-15
    • 2010-11-26
    • 2022-12-03
    • 2011-11-02
    • 1970-01-01
    • 1970-01-01
    • 2018-04-28
    • 1970-01-01
    相关资源
    最近更新 更多