总结
malloc 提供的读取未初始化内存的行为并非未定义本身。如果使用非字符类型读取包含陷阱表示的内存,则可能导致未定义的行为,但只有当类型具有陷阱表示时才会发生这种情况。 (大多数现代 C 实现没有整数类型的陷阱表示。)
然而,虽然它不是完全未定义的,但也不是完全定义的。实际读取内存不需要尝试读取未初始化的内存。
详情
C 2018 7.22.3.4 2 说,malloc 函数与参数 size:
malloc 函数为其size 由大小指定且值不确定的对象分配空间。
C 3.19.2 1 将不确定值定义为:
未指定的值或陷阱表示
C 3.19.3 1 将未指定值定义为:
本文档对在任何情况下选择哪个值没有要求的相关类型的有效值
其中没有任何东西使行为未定义。
根据 6.2.6.1 5,C 标准未定义使用非字符类型读取陷阱表示的行为。因此,如果使用具有陷阱表示的类型读取内存,则结果位碰巧包含代表陷阱的值,则行为未定义。
整数类型的陷阱表示在现代 C 实现中很少见。许多年前,一些系统会保留某些位模式,例如 16 位 800016,来表示未初始化或无效的数据,并且尝试在算术中使用这样的值会产生陷阱。在某些类型 T 中没有陷阱表示的 C 实现中,通过类型 T 访问未初始化的数据不会遇到陷阱表示。所以结果必须是类型的未指定(因此是有效的)值。
此外,C 标准中没有任何其他内容可以使这种行为未定义。 6.3.2.1 2 中有一条规则,如果未获取其地址,则访问具有自动存储持续时间的未初始化对象具有未定义的行为。但是malloc提供的内存已经分配了存储时长,不是自动的。 (该规则适用于某些 Hewlett-Packard 硬件,能够将寄存器标记为未初始化并在使用时捕获。)
此外,无论其成员的类型如何,整个结构和联合都不是陷阱表示。现代 C 实现中最常见的陷阱表示是浮点信号 NaN(非数字)。
请注意,分配的内存中的值是未指定的,上面的定义指出“本文档对在任何情况下选择哪个值没有任何要求。”这意味着如果你这样做:
unsigned *p = malloc(sizeof *p);
printf("%u\n", *p);
printf("%u\n", *p);
C 标准没有要求在第一个 printf 中为 *p 选择哪个值,也没有要求在第二个 printf 中选择哪个值,甚至没有要求它们是彼此相同。一个“未指定的值”可能表现得好像它的比特位随时都在变化。因此,行为不是未定义的——它不允许“任何事情”发生在您的程序中;你的程序不能突然跳转到不同的函数或清除其他数据——但它也没有被定义为像内存有固定值的位一样。
这意味着您无法可靠地读取未初始化的内存 - 不能保证读取内存会产生实际在物理内存中的位。
讨论
要了解为什么 C 标准允许程序表现得像内存中的位可能会发生变化,请考虑以下代码:
unsigned a = *p + 3;
unsigned b = *p + 4;
对于正常情况下的代码,编译器可能会生成这样的程序集:
// As we start, registers r7, r8, and r9 already contain p,
// the address of a, and the address of b, respectively.
load r3, (r7) // Get value of *p from memory.
add r3, #3 // Add 3.
store r3, (r8) // Store sum to a.
load r3, (r7) // Get value of *p from memory.
add r3, #4 // Add 4.
store r3, (r9) // Store sum to b.
如果内存恰好包含 0,那么这些指令会将 3 存储在 a 和 4 中 b。但是,未初始化的内存不需要表现得好像它具有固定值的规则意味着允许编译器的优化来消除加载指令。假设,这可能会导致如下指令:
add r3, #3 // Add 3.
store r3, (r8) // Store sum to a.
add r3, #4 // Add 4.
store r3, (r9) // Store sum to b.
如果此代码序列开始时r3 恰好包含0,则3 将存储在a 中,7 将存储在b 中。没有可能的值 *p 可能会导致 *p + 3 为 3 和 *p + 4 为 7。所以此代码的行为就像 *p 已自行更改。
在实践中,优化不仅会删除此处的加载指令,也不会识别后续指令也与固定值断开连接并删除它们。然而,现实世界的优化比这更复杂。 C 标准授予的许可允许编译器删除它可以找出未使用定义值的代码部分,即使它无法弄清楚程序的所有内容。