为什么定义数组之外的第一个元素默认为零?

Why does the first element outside of a defined array default to zero?

我正在准备 C++ 入门的期末考试 class。我们的教授给了我们这个问题来练习:

Explain why the code produces the following output: 120 200 16 0

using namespace std;
int main()
{
  int x[] = {120, 200, 16};
  for (int i = 0; i < 4; i++)
    cout << x[i] << " ";
}

问题的示例答案是:

The cout statement is simply cycling through the array elements whose subscript is being defined by the increment of the for loop. The element size is not defined by the array initialization. The for loop defines the size of the array, which happens to exceed the number of initialized elements, thereby defaulting to zero for the last element. The first for loop prints element 0 (120), the second prints element 1 (200), the third loop prints element 2 (16) and the forth loop prints the default array value of zero since nothing is initialized for element 3. At this point i now exceeds the condition and the for loop is terminated.

我有点困惑为什么数组外的最后一个元素总是“默认”为零。只是为了实验,我将问题中的代码粘贴到我的 IDE 中,但将 for 循环更改为 for (int i = 0; i < 8; i++)。然后输出变为 120 200 16 0 4196320 0 547306487 32655。为什么在尝试访问超出定义大小的数组中的元素时不会出现错误?程序是否只输出上次将值保存到该内存地址时的任何“剩余”数据?

它不默认为零。示例答案是错误的。未定义的行为是未定义的;该值可能是0,也可能是100。访问它可能会导致段错误,或导致您的计算机被格式化。

至于为什么不是错误,是因为不需要C++对数组进行边界检查。您可以使用向量并使用 at 函数,如果您超出范围,该函数会抛出异常,但数组不会。

I'm a bit confused as to why that last element outside of the array always "defaults" to zero.

在此声明中

int x[] = {120, 200, 16};

数组x正好有三个元素。因此访问数组边界之外的内存会调用未定义的行为。

也就是这个循环

 for (int i = 0; i < 4; i++)
 cout << x[i] << " ";

调用未定义的行为。数组最后一个元素之后的内存可以包含任何内容。

另一方面,如果数组声明为

int x[4] = {120, 200, 16};

即有四个元素,则数组中最后一个没有显式初始化的元素确实会被初始化为零。

它导致了未定义的行为,这是唯一有效的答案。编译器期望您的数组 x 恰好包含三个元素,您在读取第四个整数时在输出中看到的内容是未知的,并且在某些 systems/processors 上可能会由于尝试读取不可寻址的内存而导致硬件中断(系统不知道如何访问该地址的物理内存)。编译器可能会从堆栈中保留 x 内存,或者可能会使用寄存器(因为它非常小)。你得到 0 实际上是偶然的。通过在 clang 中使用地址清理器(-fsanitize=address 选项),您可以看到:

https://coliru.stacked-crooked.com/a/993d45532bdd4fc2

短输出是:

==9469==ERROR: AddressSanitizer: stack-buffer-overflow

您可以在编译器资源管理器中使用 un-optimized GCC: https://godbolt.org/z/8T74cr83z(包括 asm 和程序输出)
进一步研究它 在那个版本中,输出是 120 200 16 3 因为 GCC 将 i 放在数组之后的堆栈上。

您将看到 gcc 为您的数组生成以下程序集:

    mov     DWORD PTR [rbp-16], 120    # array initializer
    mov     DWORD PTR [rbp-12], 200
    mov     DWORD PTR [rbp-8], 16
    mov     DWORD PTR [rbp-4], 0       # i initializer

所以,确实有第四个元素的值为 0。但它实际上是 i 初始值设定项,并且在循环中读取时具有不同的值。编译器不会发明额外的数组元素;在他们之后充其量只有未使用的堆栈 space。

查看此示例的优化级别 - -O0 - 如此一致 - 调试最小优化;这就是 i 保存在内存中而不是调用保留寄存器的原因。开始添加优化,假设 -O1,您将得到:

    mov     DWORD PTR [rsp+4], 120
    mov     DWORD PTR [rsp+8], 200
    mov     DWORD PTR [rsp+12], 16

更多优化可能会完全优化您的数组,例如展开并仅使用立即操作数来设置对 cout.operator<< 的调用。那时未定义的行为对编译器来说是完全可见的,它必须想出办法去做。 (在其他情况下,如果数组值仅由常量(优化后)索引访问,则数组元素的寄存器在其他情况下是合理的。)

更正答案

不,它不默认为 0。这是未定义的行为。在这种情况下,这种优化和这种编译器恰好是 0。尝试访问未初始化或未分配的内存是未定义的行为。

因为它实际上是“未定义的”并且标准对此没有任何其他说明,所以您的汇编输出将不会一致。编译器可能会将数组存储在 SIMD 寄存器中,谁知道输出会是什么?

引用示例答案:

and the forth loop prints the default array value of zero since nothing is initialized for element 3

这是有史以来最错误的说法。我猜代码中有一个错字,他们想做到这一点

int x[4] = {120, 200, 16};

错误地把它 x[4] 变成了 x[]。如果不是,而且是故意的,我不知道该说什么。他们错了。

为什么不是错误?

这不是错误,因为堆栈就是这样工作的。你的应用程序不需要在堆栈中分配内存来使用它,它已经是你的了。你可以随心所欲地处理你的堆栈。当你像这样声明一个变量时:

int a;

您所做的只是告诉编译器,“我希望我的堆栈的 4 个字节用于 a,请不要将该内存用于任何其他用途。”在编译时。看这段代码:

#include <stdio.h>

int main() {
    int a;
}

程序集:

    .file   "temp.c"
    .text
    .globl  main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    endbr64
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6 /* Init stack and stuff */
    movl    [=13=], %eax
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret /* Pop the stack and return? Yes. It generated literally no code.
           All this just makes a stack, pops it and returns. Nothing. */
    .cfi_endproc /* Stuff after this is system info, and other stuff
                 we're not interested. */
.LFE0:
    .size   main, .-main
    .ident  "GCC: (Ubuntu 11.1.0-1ubuntu1~20.04) 11.1.0"
    .section    .note.GNU-stack,"",@progbits
    .section    .note.gnu.property,"a"
    .align 8
    .long   1f - 0f
    .long   4f - 1f
    .long   5
0:
    .string "GNU"
1:
    .align 8
    .long   0xc0000002
    .long   3f - 2f
2:
    .long   0x3
3:
    .align 8
4:

阅读代码中的注释进行解释。

因此,您可以看到 int x; 什么也没做。如果我打开优化,编译器甚至不会费心制作堆栈并做所有这些事情,而是直接 return。 int x; 只是一个编译时命令,编译器会说:

x is a variable that is a signed int. It needs 4 bytes, please continue declaration after skipping these 4 bytes(and alignment).

(堆栈的)高级语言中的变量的存在只是为了使堆栈的“分布”更加系统化,并且以一种可读的方式。变量的声明不是运行时间的过程。它只是教编译器如何在变量之间分配堆栈并相应地准备程序。执行时,程序会分配一个堆栈(这是一个 运行 时间的过程),但它已经硬编码了哪些变量获得堆栈的哪些部分。例如。变量 a 可能从 -0(%rbp)-4(%rbp),而 b 可能从 -5(%rbp)-8(%rbp)。这些值是在编译时确定的。变量名在编译时也不存在,它们只是教编译器如何准备程序以使用其堆栈的一种方式。

作为用户,您可以随心所欲地使用堆栈;但你可能不会。您应该始终声明变量或数组以让编译器知道。

边界检查

在像 Go 这样的语言中,即使您的堆栈是您的,编译器也会插入额外的检查以确保您没有意外使用未声明的内存。出于性能原因,它没有在 C 和 C++ 中完成,它会导致可怕的未定义行为和分段错误更频繁地发生。

堆和数据部分

堆是存储大量数据的地方。这里没有存储变量,只有数据;并且您的一个或多个变量将包含指向该数据的指针。如果你使用你没有分配的东西(在 运行 时间完成),你会得到一个分段错误。

数据部分是另一个可以存储内容的地方。变量可以存储在这里。它与您的代码一起存储,因此超出分配是非常危险的,因为您可能会不小心修改程序的代码。因为它与您的代码一起存储,所以它显然也在编译时分配。我其实不太了解数据部分的内存安全。显然,您可以在没有 OS 抱怨的情况下超过它,但我不知道更多,因为我不是系统黑客,也没有将其用于恶意目的的可疑目的。基本上,我不知道数据部分是否超出分配。希望有人对此发表评论(或回答)。

上面显示的所有程序集都是在 Ubuntu 机器上由 GCC 11.1 编译的 C。它使用 C 而不是 C++ 来提高可读性。

The element size is not defined by the array initialization. The for loop defines the size of the array, which happens to exceed the number of initialized elements, thereby defaulting to zero for the last element.

这是完全错误的。来自 C++17 standard 的第 11.6.1p5 节:

An array of unknown bound initialized with a brace-enclosed initializer-list containing n initializer-clauses, where n shall be greater than zero, is defined as having n elements (11.3.4). [ Example:

int x[] = { 1, 3, 5 };

declares and initializes x as a one-dimensional array that has three elements since no size was specified and there are three initializers. — end example ]

因此对于没有显式大小的数组,初始化器定义数组的大小。 for 循环读取数组末尾,这样做会触发 undefined behavior.

为不存在的第 4 个元素打印 0 的事实只是未定义行为的表现。无法保证该值会被打印出来。事实上,当我 运行 这个程序时,我用 -O0 编译时得到 3 最后一个值,用 -O1.

编译时得到 0