在C语言中,声明但未初始化的变量会发生什么?它有一个值吗?

175

如果我在C语言中写:

int num;

在我给 num 分配任何值之前,num 的值是否未确定?


5
“嗯,那不是一个声明的变量,而是一个定义的变量吗?(如果我在展现我的 C++ 知识,请见谅…)” - sbi
8
我可以声明一个变量而不定义它:extern int x; 但是定义总是意味着声明。在C++中,静态类成员变量可以在不声明的情况下进行定义,因为声明必须在类定义(而不是声明!)中,而定义必须在类定义之外。 - bdonlan
看起来,定义的意思是你也必须对其进行初始化。 - atp
9个回答

233
静态变量(文件作用域和函数静态)被初始化为零。
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
}

在实践中,它们往往最初只有一些无意义的值 - 一些编译器甚至可能放入特定的固定值,以便在调试器中查看时显而易见 - 但严格来说,编译器可以任意采取行动,从崩溃到通过鼻腔召唤恶魔
至于为什么它是未定义行为,而不仅仅是“未定义/任意值”,原因是许多CPU体系结构在其表示中具有各种类型的附加标志位。现代的一个例子是Itanium,在其寄存器中具有“非事物”位; 当然,C标准制定者考虑了一些较旧的体系结构。
尝试使用设置了这些标志位的值可能会导致CPU异常,在一个本不应该失败的操作中(例如整数加法或分配给另一个变量)。如果您离开一个变量未初始化,编译器可能会选择一些随机垃圾并设置这些标志位 - 这意味着触摸该未初始化变量可能是致命的。

3
哦不,它们并不是那样。如果你很幸运,在有R的月份里,在非面对客户、处于调试模式时,可能会这样。 - Martin Beckett
8
根据ISO/IEC 9899:1999 6.7.8#10标准规定,静态初始化是必需的。这里用“静态初始化”翻译了“static initialization”。 - bdonlan
3
第一个例子就我所知道的来说还不错。但是对于第二个例子编译器为什么会崩溃我就不太清楚了 :) - user172783
6
@Stuart:有一个叫做“陷阱表示法”的东西,它基本上是一种不能表示有效值的位模式,可能会在运行时导致硬件异常。唯一保证任何位模式都是有效值的C类型是“char”;其他所有类型都可能有陷阱表示法。或者,由于访问未初始化的变量无论如何都是未定义的行为,符合规范的编译器可能会简单地进行一些检查,并决定发出信号来指示问题。 - Pavel Minaev
5
bdonian是正确的。 C语言一直以来都被相当精确地规定着。在C89和C99之前,dmr在20世纪70年代早期就详细说明了所有这些事情。即使在最原始的嵌入式系统中,只需要一个memset()函数就可以做到正确,因此没有不符合标准的环境的借口。我已经在我的回答中引用了该标准。 - DigitalRoss
显示剩余17条评论

64

如果存储类为静态或全局,则为0,如果存储类为自动,则为不确定

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


3
不确定通常意味着它可以做任何事情。它可以是零,可以是原来在那里的值,也可以使程序崩溃,甚至可以让计算机从CD插槽中制作蓝莓煎饼。你绝对没有保证。它可能会导致行星的毁灭。至少在规范上是这样的...任何制造类似效果编译器的人都将受到高度谴责 B-) - Brian Postow
在 C11 N1570 草案中,可以在 3.19.2 找到 未定值 的定义。 - user3528438
1
它是否总是取决于编译器或操作系统设置静态变量的值?例如,如果有人编写了自己的操作系统或编译器,并且如果它们也默认将静态变量的初始值设置为不确定,那么这是否可能? - Aditya Singh
1
@AdityaSingh,操作系统可以使编译器更容易运行世界上现有的 C 代码目录,但最终这是编译器的主要职责,并且其次是符合标准。当然可以以不同的方式实现,但是为什么?另外,要使静态数据不确定很棘手,因为基于安全原因,操作系统真的希望先将页面清零。 (自动变量只是表面上不可预测,因为您自己的程序通常在较早的时间点使用这些堆栈地址。) - DigitalRoss
@BrianPostow 不,那不正确。请参见 https://dev59.com/v2ct5IYBdhLWcg3wmOng#40674888。使用不确定值会导致未指定的行为,而非未定义的行为,除了陷阱表示的情况。 - Lundin

15

这取决于变量的存储持续时间。具有静态存储持续时间的变量始终隐式地初始化为零。

至于自动(局部)变量,未初始化的变量具有不确定值。不确定值,除其他外,意味着您可能在该变量中看到的任何“值”不仅是不可预测的,而且甚至不保证稳定。例如,在实践中(即暂时忽略UB),此代码

int num;
int a = num;
int b = num;

不能保证变量ab会收到相同的值。有趣的是,这不是一些学究式的理论概念,由于优化的结果,在实践中很容易发生。

因此,通常流行的答案“它被初始化为存储器中存在的任何垃圾”甚至都不正确。 未初始化 变量的行为与使用垃圾 初始化 的变量的行为不同。


我不明白(好吧,我其实是明白的)为什么这个帖子的赞数比DigitalRoss的帖子少得多,而且只相差一分钟 :D - Antti Haapala -- Слава Україні

9

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);
}

请注意,GCC 11 似乎会产生不同的汇编输出,上述代码在“工作”之后停止,“毕竟这是未定义的行为”:为什么-gcc中的-O3似乎会将我的本地变量初始化为0,而-O0则不会?-O3 中的本地变量
实现分析请参见:GDB 中 <value optimized out> 是什么意思? 全局变量
标准:0
实现:.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内核将程序加载到内存中并启动时,它将清零该内存区域。


1
我相信 gcc -00 -std=c99 a.c 应该是 gcc -O0 -std=c99 a.c - jian
希望不会打扰到您。我正在通过https://linuxhint.com/pipe_system_call_c/学习管道。我确实进行了很多谷歌搜索,但是我没有找到官方参考资料表明文件描述符3表示读取,4表示写入。显然,我可以从维基百科上获得0,1,2。 - jian
@Mark 不用担心。你为什么关心文件描述符的确切值是3/4呢?因为你可以通过pipe()调用的返回值获得它们。例如,参见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

3

这要看情况而定。如果这个定义是全局的(在任何函数之外),那么num将被初始化为零。如果它是局部的(在一个函数内部),那么它的值是不确定的。理论上,即使尝试读取该值也会有未定义的行为——C允许存在不参与值的位,但必须以特定的方式设置才能从读取变量中获得定义的结果。


0
因为计算机的存储容量是有限的,自动变量通常会被保存在之前用于其他任意目的的存储单元中(无论是寄存器还是RAM)。如果在给这样一个变量赋值之前使用它,那么该存储单元可能会保留先前的内容,因此变量的内容将是不可预测的。
另外一个问题是,许多编译器可能会将变量保存在比相关类型更大的寄存器中。尽管编译器需要确保写入变量并读回时,任何值都会被截断和/或符号扩展到其正确的大小,但许多编译器会在写入变量时执行这种截断,并期望在读取变量之前已经执行了截断。在这样的编译器上,类似以下代码的情况:
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()。由于xhey中不需要,并且函数应该将返回值放入寄存器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);      
}

编译器可以推断,因为使用大于3的模式调用moo()将不可避免地导致程序调用未定义行为,所以编译器可以省略任何只与mode大于等于4相关的代码,比如通常会阻止在这种情况下发射核弹的代码。请注意,无论是标准还是现代编译器理念,都不会关心"hey"的返回值被忽略的事实--试图返回它的行为给予编译器生成任意代码的无限权力。

在你的例子中,我没有看到你在答案中提到的任何foo()函数。 - CITBL

0

基本答案是,是未定义的。

如果您因此看到奇怪的行为,可能取决于它在哪里声明。如果在堆栈上的函数内部,则每次调用该函数时内容很可能会有所不同。如果它是静态或模块范围,则未定义但不会更改。


-1

如果存储类为静态或全局,则在加载时,BSS初始化变量或内存位置(ML)为0,除非该变量最初被赋予某个值。对于本地未初始化的变量,将陷阱表示分配给内存位置。因此,如果编译器覆盖了包含重要信息的寄存器,则程序可能会崩溃。

但是,一些编译器可能具有避免此类问题的机制。

我曾与NEC V850系列一起工作,当我意识到存在陷阱表示时,它具有代表数据类型的未定义值的位模式,除了char外。当我取一个未初始化的char时,由于陷阱表示,我得到了零默认值。这可能对任何使用necv850es的人有用。


如果您在使用无符号字符时遇到陷阱表示,那么您的系统就不符合要求。根据C17 6.2.6.1/5规定,它们明确禁止包含陷阱表示。 - Lundin

-4

就我所知,这主要取决于编译器,但通常情况下,编译器会预设值为0。
在VC++中,我得到了垃圾值,而TC则给出了0的值。 我像下面这样打印它

int i;
printf('%d',i);

如果您得到一个确定性的值,例如 0,您的编译器很可能会采取额外的步骤来确保它获取该值(通过添加代码来初始化变量)。一些编译器在进行“调试”编译时会这样做,但选择值 0 是一个坏主意,因为它会隐藏代码中的错误(更合适的做法是保证一个真正不可能的数字,比如 0xBAADF00D 或类似的东西)。我认为大多数编译器只会将占用内存的垃圾作为变量的值留下来(即通常不会被视为 0)。 - skyking

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接