如果我在C语言中写:
int num;
在我给 num
分配任何值之前,num
的值是否未确定?
如果我在C语言中写:
int num;
在我给 num
分配任何值之前,num
的值是否未确定?
int x; // zero
int y = 0; // also zero
void foo() {
static int x; // also zero
}
非静态变量(局部变量)是不确定的。在赋值之前读取它们会导致未定义的行为。
void foo() {
int x;
printf("%d", x); // the compiler is free to crash here
}
C语言一直非常明确关于对象的初始值。如果是全局或static
,它们将被清零。如果是auto
,则其值为不确定的。
在C89编译器之前就是这种情况,并由K&R和DMR的原始C报告进行了规定。
这也是在C89中的情况,参见第6.5.7节 初始化。
如果具有自动存储期的对象未显式初始化,则其值为不确定。
如果具有静态存储期的对象未显式初始化,则像每个具有算术类型的成员都分配0,每个具有指针类型的成员都分配null指针常量那样隐式初始化。
在C99中也是这种情况,参见第6.7.8节 初始化。
如果具有自动存储期的对象未显式初始化,则其值为不确定。
如果具有静态存储期的对象未显式初始化,则:
— 如果它具有指针类型,则初始化为null指针;
— 如果它具有算术类型,则初始化为(正数或无符号)零;
— 如果它是一个聚合体,则每个成员都根据这些规则递归地初始化;
— 如果它是联合体,则第一个命名成员将按照这些规则递归初始化。
至于indeterminate具体意味着什么,对于C89来说我不确定,C99指出:
3.17.2
indeterminate value
是未指定值或陷阱表示。
但是在现实生活中,每个堆栈页面实际上都从零开始,但是当您的程序查看任何auto
存储类值时,它会看到上次使用这些堆栈地址时您自己的程序留下的内容。如果您分配了许多auto
数组,您最终会看到它们以零整齐地开始。
也许您会想,为什么会这样呢?另一个SO答案解决了这个问题,请参见:https://dev59.com/-nI95IYBdhLWcg3w7CjL#2091505
未定值
的定义。 - user3528438这取决于变量的存储持续时间。具有静态存储持续时间的变量始终隐式地初始化为零。
至于自动(局部)变量,未初始化的变量具有不确定值。不确定值,除其他外,意味着您可能在该变量中看到的任何“值”不仅是不可预测的,而且甚至不保证稳定。例如,在实践中(即暂时忽略UB),此代码
int num;
int a = num;
int b = num;
不能保证变量a
和b
会收到相同的值。有趣的是,这不是一些学究式的理论概念,由于优化的结果,在实践中很容易发生。
因此,通常流行的答案“它被初始化为存储器中存在的任何垃圾”甚至都不正确。 未初始化 变量的行为与使用垃圾 初始化 的变量的行为不同。
Ubuntu 15.10, Kernel 4.2.0, x86-64, GCC 5.2.1 示例
够了的标准,让我们看看一个实现 :-)
局部变量
标准:未定义的行为。
实现:程序分配堆栈空间,并从未将任何内容移动到该地址,因此先前存在的任何内容都将被使用。
#include <stdio.h>
int main() {
int i;
printf("%d\n", i);
}
使用以下命令进行编译:
gcc -O0 -std=c99 a.c
输出:
0
并使用以下命令进行反编译:
objdump -dr a.out
to:
0000000000400536 <main>:
400536: 55 push %rbp
400537: 48 89 e5 mov %rsp,%rbp
40053a: 48 83 ec 10 sub $0x10,%rsp
40053e: 8b 45 fc mov -0x4(%rbp),%eax
400541: 89 c6 mov %eax,%esi
400543: bf e4 05 40 00 mov $0x4005e4,%edi
400548: b8 00 00 00 00 mov $0x0,%eax
40054d: e8 be fe ff ff callq 400410 <printf@plt>
400552: b8 00 00 00 00 mov $0x0,%eax
400557: c9 leaveq
400558: c3 retq
根据我们对x86-64调用约定的了解:
%rdi
是第一个printf参数,因此字符串"%d\n"
位于地址0x4005e4
%rsi
是第二个printf参数,因此是i
。
它来自于-0x4(%rbp)
,这是第一个4字节的本地变量。
此时,rbp
在内核分配的堆栈的第一页中,因此要理解该值,我们需要查看内核代码并找出它设置为什么。
待办事项:当进程死亡时,内核是否会在重用它之前将该内存设置为某些内容?如果没有,新进程将能够读取其他已完成程序的内存,泄漏数据。参见:未初始化的值是否会带来安全风险?
然后我们还可以进行自己的堆栈修改,并编写有趣的内容,例如:
#include <assert.h>
int f() {
int i = 13;
return i;
}
int g() {
int i;
return i;
}
int main() {
f();
assert(g() == 13);
}
-O3
中的本地变量.bss
部分。#include <stdio.h>
int i;
int main() {
printf("%d\n", i);
}
gcc -O0 -std=c99 a.c
编译为:
0000000000400536 <main>:
400536: 55 push %rbp
400537: 48 89 e5 mov %rsp,%rbp
40053a: 8b 05 04 0b 20 00 mov 0x200b04(%rip),%eax # 601044 <i>
400540: 89 c6 mov %eax,%esi
400542: bf e4 05 40 00 mov $0x4005e4,%edi
400547: b8 00 00 00 00 mov $0x0,%eax
40054c: e8 bf fe ff ff callq 400410 <printf@plt>
400551: b8 00 00 00 00 mov $0x0,%eax
400556: 5d pop %rbp
400557: c3 retq
400558: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1)
40055f: 00
# 601044 <i>
表示 i
存储在地址 0x601044
中,并且:
readelf -SW a.out
[25] .bss NOBITS 0000000000601040 001040 000008 00 WA 0 0 4
该段文字表明0x601044
位于.bss
部分的中间,该部分从0x601040
开始,长度为8个字节。
ELF标准保证了名为.bss
的部分完全由零填充:
.bss
这个部分包含了对程序内存映像有贡献的未初始化数据。根据定义,系统会在程序开始运行时将数据初始化为零。如节类型SHT_NOBITS
所示,该部分不占用文件空间。
此外,类型SHT_NOBITS
高效并且不占用可执行文件的空间:
sh_size
此成员以字节为单位给出了该部分的大小。除非节类型是SHT_NOBITS
,否则该部分将在文件中占用sh_size
字节。类型为SHT_NOBITS
的节可能具有非零大小,但它在文件中不占用空间。
然后,当Linux内核将程序加载到内存中并启动时,它将清零该内存区域。
gcc -00 -std=c99 a.c
应该是 gcc -O0 -std=c99 a.c
? - jianpipe()
调用的返回值获得它们。例如,参见man pipe
中的示例。很可能Linux内核选择了3/4,因为0、1和2已经被占用了,所以它只是递增。当然,下一个管道很可能会有5/6。你可以尝试阅读Linux内核源代码来确认这一点。很可能POSIX没有指定确切的值:https://pubs.opengroup.org/onlinepubs/9699919799/functions/pipe.html,因此依赖它们不具备可移植性。 - Ciro Santilli OurBigBook.com这要看情况而定。如果这个定义是全局的(在任何函数之外),那么num
将被初始化为零。如果它是局部的(在一个函数内部),那么它的值是不确定的。理论上,即使尝试读取该值也会有未定义的行为——C允许存在不参与值的位,但必须以特定的方式设置才能从读取变量中获得定义的结果。
uint16_t hey(uint32_t x, uint32_t mode)
{
uint16_t q;
if (mode==1) q=2;
if (mode==3) q=4;
return q;
}
uint32_t wow(uint32_t mode)
{
return hey(1234567, mode);
}
wow()
将值1234567分别存储到寄存器0和1中,并调用hey()
。由于x
在hey
中不需要,并且函数应该将返回值放入寄存器0中,编译器可能会将寄存器0分配给q
。如果mode
为1或3,则寄存器0将被加载为2或4,但如果它是其他值,则函数可能会返回寄存器0中的任何值(即值1234567),尽管该值不在uint16_t的范围内。
为了避免要求编译器进行额外工作以确保未初始化变量似乎从未持有超出其域的值,并避免需要详细说明不确定行为,标准规定使用未初始化的自动变量是未定义行为。在某些情况下,这样做的后果可能比类型范围之外的值更令人惊讶。例如,给定:
void moo(int mode)
{
if (mode < 5)
launch_nukes();
hey(0, mode);
}
moo()
将不可避免地导致程序调用未定义行为,所以编译器可以省略任何只与mode
大于等于4相关的代码,比如通常会阻止在这种情况下发射核弹的代码。请注意,无论是标准还是现代编译器理念,都不会关心"hey"的返回值被忽略的事实--试图返回它的行为给予编译器生成任意代码的无限权力。基本答案是,是未定义的。
如果您因此看到奇怪的行为,可能取决于它在哪里声明。如果在堆栈上的函数内部,则每次调用该函数时内容很可能会有所不同。如果它是静态或模块范围,则未定义但不会更改。
如果存储类为静态或全局,则在加载时,BSS初始化变量或内存位置(ML)为0,除非该变量最初被赋予某个值。对于本地未初始化的变量,将陷阱表示分配给内存位置。因此,如果编译器覆盖了包含重要信息的寄存器,则程序可能会崩溃。
但是,一些编译器可能具有避免此类问题的机制。
我曾与NEC V850系列一起工作,当我意识到存在陷阱表示时,它具有代表数据类型的未定义值的位模式,除了char外。当我取一个未初始化的char时,由于陷阱表示,我得到了零默认值。这可能对任何使用necv850es的人有用。
就我所知,这主要取决于编译器,但通常情况下,编译器会预设值为0。
在VC++中,我得到了垃圾值,而TC则给出了0的值。
我像下面这样打印它
int i;
printf('%d',i);
0
,您的编译器很可能会采取额外的步骤来确保它获取该值(通过添加代码来初始化变量)。一些编译器在进行“调试”编译时会这样做,但选择值 0
是一个坏主意,因为它会隐藏代码中的错误(更合适的做法是保证一个真正不可能的数字,比如 0xBAADF00D
或类似的东西)。我认为大多数编译器只会将占用内存的垃圾作为变量的值留下来(即通常不会被视为 0
)。 - skyking
extern int x;
但是定义总是意味着声明。在C++中,静态类成员变量可以在不声明的情况下进行定义,因为声明必须在类定义(而不是声明!)中,而定义必须在类定义之外。 - bdonlan