cia*_*anc 92 c++ arrays initialization undefined-behavior zero-initialization
我正在准备 C++ 入门课程的期末考试。我们的教授给了我们这个问题来练习:
解释为什么代码会产生以下输出:
120 200 16 0
using namespace std;
int main()
{
int x[] = {120, 200, 16};
for (int i = 0; i < 4; i++)
cout << x[i] << " ";
}
Run Code Online (Sandbox Code Playgroud)
该问题的示例答案是:
cout 语句只是循环遍历其下标由 for 循环的增量定义的数组元素。元素大小不是由数组初始化定义的。for 循环定义了数组的大小,该大小恰好超出了初始化元素的数量,因此最后一个元素默认为零。第一个 for 循环打印元素 0 (120),第二个循环打印元素 1 (200),第三个循环打印元素 2 (16),第四个循环打印默认数组值零,因为元素 3 没有任何初始化。现在 i 点超出了条件并且 for 循环终止。
我有点困惑为什么数组之外的最后一个元素总是“默认”为零。为了进行实验,我将问题中的代码粘贴到我的 IDE 中,但将 for 循环更改为for (int i = 0; i < 8; i++). 然后输出更改为120 200 16 0 4196320 0 547306487 32655. 为什么尝试访问超出定义大小的数组中的元素时不会出现错误?程序是否只输出上次将值保存到该内存地址时的“剩余”数据?
Vla*_*cow 97
我有点困惑为什么数组之外的最后一个元素总是“默认”为零。
在这份声明中
int x[] = {120, 200, 16};
Run Code Online (Sandbox Code Playgroud)
该数组x恰好包含三个元素。因此,访问数组边界之外的内存会调用未定义的行为。
也就是说,这个循环
for (int i = 0; i < 4; i++)
cout << x[i] << " ";
Run Code Online (Sandbox Code Playgroud)
调用未定义的行为。数组最后一个元素之后的内存可以包含任何内容。
另一方面,如果数组被声明为
int x[4] = {120, 200, 16};
Run Code Online (Sandbox Code Playgroud)
也就是说,如果有四个元素,那么数组中没有显式初始化器的最后一个元素确实会被初始化为零。
Chr*_*sMM 51
它不默认为零。示例答案是错误的。未定义的行为是未定义的;该值可能是0,也可能是100。访问它可能会导致段错误,或者导致您的计算机被格式化。
至于为什么不是错误,是因为C++不需要对数组进行边界检查。您可以使用向量并使用该at函数,如果超出界限,该函数会引发异常,但数组不会。
mar*_*inj 31
它导致未定义的行为,这是唯一有效的答案。编译器期望您的数组x恰好包含三个元素,读取第四个整数时您在输出中看到的内容是未知的,并且在某些系统/处理器上可能会因尝试读取不可寻址的内存而导致硬件中断(系统不知道如何访问该地址的物理内存)。编译器可能会从堆栈中保留x内存,或者可能使用寄存器(因为它非常小)。你得到0的事实实际上是偶然的。通过在 clang 中使用地址清理程序(-fsanitize=address 选项),您可以看到以下内容:
https://coliru.stacked-crooked.com/a/993d45532bdd4fc2
短输出是:
==9469==ERROR: AddressSanitizer: stack-buffer-overflow
Run Code Online (Sandbox Code Playgroud)
您可以在编译器资源管理器上使用未优化的 GCC进一步研究它: https://godbolt.org/z/8T74cr83z(包括 asm 和程序输出)
在该版本中,输出是120 200 16 3因为 GCCi在大批。
您将看到 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
Run Code Online (Sandbox Code Playgroud)
所以,确实有第四个元素的值为 0。但它实际上是i初始化器,并且在循环中读取时具有不同的值。编译器不会发明额外的数组元素;在最好的情况下,它们之后只会有未使用的堆栈空间。
请参阅此示例的优化级别 - 它的-O0- 如此一致 - 调试最小优化;这就是为什么i保存在内存中而不是调用保留的寄存器中。开始添加优化,假设-O1您将得到:
mov DWORD PTR [rsp+4], 120
mov DWORD PTR [rsp+8], 200
mov DWORD PTR [rsp+12], 16
Run Code Online (Sandbox Code Playgroud)
更多优化可能会完全优化您的数组,例如展开并仅使用立即操作数来设置对cout.operator<<. 那时,未定义的行为对编译器来说是完全可见的,并且它必须想出一些办法来做。(在其他情况下,如果数组值仅由常量(优化后)索引访问,则数组元素的寄存器是合理的。)
Sha*_*tam 12
不,它不默认为 0。这是未定义的行为。在这种情况、这种优化和这种编译器下,它恰好是 0。尝试访问未初始化或未分配的内存是未定义的行为。
因为它实际上是“未定义的”,并且标准对此没有其他说明,所以您的程序集输出将不一致。编译器可能会将数组存储在 SIMD 寄存器中,谁知道输出会是什么?
引用示例答案:
第四个循环打印默认数组值零,因为元素 3 没有初始化
这是有史以来最错误的说法。我猜代码中有一个错别字,他们想把它改过来
int x[4] = {120, 200, 16};
Run Code Online (Sandbox Code Playgroud)
并错误地把它变成x[4]了 just x[]。如果不是,而且是故意的,我不知道该说什么。他们错了。
这不是一个错误,因为堆栈就是这样工作的。您的应用程序不需要在堆栈中分配内存来使用它,它已经是您的了。您可以按照自己的意愿对堆栈进行任何操作。当你像这样声明一个变量时:
int a;
Run Code Online (Sandbox Code Playgroud)
你所做的就是告诉编译器,“我想要我的堆栈的 4 个字节用于a,请不要将该内存用于其他任何事情。” 在编译时。看这段代码:
#include <stdio.h>
int main() {
int a;
}
Run Code Online (Sandbox Code Playgroud)
集会:
.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 $0, %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:
Run Code Online (Sandbox Code Playgroud)
阅读代码中的注释以获取解释。
所以,你可以看到int x;什么也没做。如果我打开优化,编译器甚至不会费心创建堆栈并执行所有这些操作,而是直接返回。int x;只是向编译器发出的编译时命令:
x 是一个有符号整型变量。它需要4个字节,请跳过这4个字节(和对齐)后继续声明。
高级语言中(堆栈的)变量的存在只是为了使堆栈的“分布”更加系统化并且以一种可读的方式。变量的声明不是运行时过程。它只是教编译器如何在变量之间分配堆栈并相应地准备程序。执行时,程序分配一个堆栈(这是一个运行时进程),但它已经硬编码为哪些变量获得堆栈的哪一部分。例如。变量a可能会到达-0(%rbp),-4(%rbp)而b会-5(%rbp)到达-8(%rbp)。这些值是在编译时确定的。变量的名称在编译时也不存在,它们只是教编译器如何准备程序以使用其堆栈的一种方法。
作为用户,您可以随意使用堆栈;但你可能不会。您应该始终声明变量或数组以让编译器知道。
在像 Go 这样的语言中,即使你的堆栈是你自己的,编译器也会插入额外的检查以确保你不会意外使用未声明的内存。出于性能原因,C 和 C++ 中没有这样做,并且它会导致可怕的未定义行为和分段错误更频繁地发生。
堆是存储大量数据的地方。这里不存储变量,只存储数据;并且一个或多个变量将包含指向该数据的指针。如果您使用尚未分配的内容(在运行时完成),则会出现分段错误。
数据部分是另一个可以存储内容的地方。变量可以存储在这里。它与您的代码一起存储,因此超出分配是非常危险的,因为您可能会意外修改程序的代码。由于它与代码一起存储,因此显然也在编译时分配。我实际上对数据部分的内存安全了解不多。显然,你可以超越它,而不会引起操作系统的抱怨,但我不知道更多,因为我不是系统黑客,也没有使用它来进行恶意目的的可疑目的。基本上,我不知道数据部分的分配是否超出。希望有人对此发表评论(或回答)。
上面显示的所有程序集都是在 Ubuntu 机器上由 GCC 11.1 编译成 C 语言的。它是用 C 而不是 C++ 编写的,以提高可读性。
\n\n元素大小不是由数组初始化定义的。for 循环定义了数组的大小,该大小恰好超出了初始化元素的数量,因此最后一个元素默认为零。
\n
这是完全错误的。来自C++17 标准第 11.6.1p5 节:
\n\n\n使用包含初始化子句的大括号括起来的初始化器列表初始化的未知边界
\nn数组,其中应n大于零,被定义为具有 n 个元素 (11.3.4)。[\n示例:Run Code Online (Sandbox Code Playgroud)\nint x[] = { 1, 3, 5 };\n将 x 声明并初始化为具有三个\n元素的一维数组,因为未指定大小并且存在三个初始值设定项。\n\xe2\x80\x94结束示例]
\n
因此,对于没有明确大小的数组,初始化器定义数组的大小。循环for读取超出数组末尾的内容,这样做会触发未定义的行为。
事实上,0 是为不存在的第四个元素打印的,这只是未定义行为的表现。无法保证该值会被打印。事实上,当我运行这个程序时,我在使用 编译时得到最后一个值 3,在-O0使用 编译时得到 0 -O1。
| 归档时间: |
|
| 查看次数: |
6656 次 |
| 最近记录: |