在C中声明的,未初始化的变量会怎样?有价值吗?


138

如果在CI中写:

int num;

在将任何东西分配给之前num,值是num不确定的吗?


4
嗯,那不是定义的变量,不是声明的变量吗?(很抱歉,如果这是我的C ++闪闪发光……)
sbi

6
不可以。我可以在不定义变量的情况下声明变量:extern int x;但是,定义总是意味着要声明。在C ++中不是这样,因为静态类成员变量可以在不声明的情况下进行定义,因为声明必须在类定义中(而不是声明!),并且该定义必须在类定义之外。
bdonlan

ee.hawaii.edu/~tep/EE160/Book/chap14/subsection2.1.1.4.html 看起来,已定义意味着您也必须对其进行初始化。
atp

Answers:


187

静态变量(文件作用域和函数静态)初始化为零:

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,它的寄存器中有一个“ Not Thing”位。当然,C标准起草者正在考虑一些较旧的体系结构。

尝试使用设置了这些标志位的值可能会导致CPU异常,而该异常实际上不会失败(例如,整数加法或分配给另一个变量)。而且,如果您保留未初始化的变量,则编译器可能会在设置了这些标志位的情况下拾取一些随机垃圾,这意味着触摸未初始化的变量可能是致命的。


2
哦,不,不是。如果您很幸运,它们可能会处于调试模式,当您不在客户面前时,如果您很幸运,则需要几个月的时间
Martin Beckett

8
不是什么 标准要求静态初始化;参见ISO / IEC 9899:1999 6.7.8#10
bdonlan

2
据我所知,第一个例子很好。我不知道为什么编译器可能在第二个崩溃:)

6
@Stuart:有一个叫做“陷阱表示”的东西,它基本上是一个位模式,不表示有效值,并且可能在运行时导致例如硬件异常。可以保证任何位模式都是有效值的唯一C类型是char;。其他所有对象都有陷阱表示。或者-由于访问未初始化的变量无论如何都是UB-合格的编译器可能只是进行一些检查并决定发出问题。
帕维尔·米纳夫

5
bdonian是正确的。C总是被精确地指定。在C89和C99之前,dmr的论文在1970年代初期指定了所有这些内容。即使在最原始的嵌入式系统中,也只需要一个memset()即可正确执行操作,因此对于不合格的环境没有任何借口。我在回答中引用了标准。
DigitalRoss

57

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

C对于对象的初始值一直很具体。如果是global或static,它们将被清零。如果为auto,则该值不确定

在C89之前的编译器中就是这种情况,这由K&R和DMR的原始C报告指定。

在C89中就是这种情况,请参见第6.5.7节“ 初始化”

如果没有明确初始化具有自动存储期限的对象,则其值不确定。如果未明确初始化具有静态存储持续时间的对象,则将隐式初始化该对象,就好像每个具有算术类型的成员都被分配了0,而每个具有指针类型的成员都被分配了一个空指针常量。

在C99中就是这种情况,请参见第6.7.8节“ 初始化”

如果未自动初始化具有自动存储期限的对象,则其值不确定。如果未明确初始化具有静态存储持续时间的对象,则:
— —如果具有指针类型,则将其初始化为空指针;
—如果具有算术类型,则将其初始化为(正数或无符号)零;
—如果是聚合,则根据这些规则(递归)初始化每个成员;
—如果它是一个联合,则将根据这些规则(递归)初始化第一个命名成员。

至于究竟是什么不确定的手段,我不知道的C89,C99说:

3.17.2
不确定值

(未指定值)或陷阱表示

但是,不管标准说什么,在现实生活中,每个堆栈页面实际上都是从零开始的,但是当您的程序查看任何auto存储类值时,它将看到您自己的程序最后一次使用这些堆栈地址时所留下的内容。如果您分配了很多auto数组,您将看到它们最终以零开始整洁。

您可能想知道,为什么会这样呢?一个不同的SO答案可解决该问题,请参阅:https : //stackoverflow.com/a/2091505/140740


3
通常不确定(用于?)表示它可以执行任何操作。它可以是零,可以是其中的值,它可以使程序崩溃,它可以使计算机从CD插槽中产生蓝莓煎饼。您绝对没有任何保证。它可能导致地球的毁灭。至少尽可能的规范去... ...任何人谁做了一个编译器,它实际上做了类似的事情会在B超高度皱起了眉头)
布赖恩Postow

在C11 N1570草案中,的定义indeterminate value可以在3.19.2中找到。
user3528438 '16

是否总是依赖于编译器或OS为静态变量设置什么值?例如,如果有人编写了我自己的OS或编译器,并且如果他们也默认将static的初始值设置为不确定,那可能吗?
Aditya Singh

1
@AdityaSingh,操作系统可以使它更容易的编译器,但最终它的编译器的主要责任,运行世界上现有的C代码目录和次要责任,以达到标准。当然可以以不同的方式来做,但是为什么呢?另外,使静态数据不确定是很棘手的,因为出于安全原因,操作系统确实要首先将页面清零。(自动变量只是表面上不可预测的,因为您自己的程序通常在更早的时候就使用了这些堆栈地址。)
DigitalRoss

@BrianPostow不,那是不正确的。请参阅stackoverflow.com/a/40674888/584518。使用不确定的值会导致未指定的行为,而不是未定义的行为,但陷阱表示的情况除外。
隆丁

11

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

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

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

不保证变量ab将获得相同的值。有趣的是,这不是一些古怪的理论概念,在实践中,由于优化而容易发生这种情况。

因此,通常来说,“用内存中的任何垃圾初始化它”的流行答案甚至都不是正确的。未初始化的变量的行为是一个变量的不同初始化垃圾。


我无法理解(我很好很好)为什么要少得多upvotes比一个从DigitalRoss只需一分钟后:d
安蒂·哈帕拉

7

Ubuntu 15.10,内核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

至:

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内核已在堆栈的第一页中分配了该值,因此要了解该值,我们将调查内核代码并找出将其设置为什么。

    某个进程死后,TODO内核会将该内存设置为某种内存,然后再将其用于其他进程吗?如果没有,新进程将能够读取其他已完成程序的内存,从而泄漏数据。请参阅:未初始化的值是否存在安全风险?

然后,我们还可以玩自己的堆栈修改并编写有趣的东西,例如:

#include <assert.h>

int f() {
    int i = 13;
    return i;
}

int g() {
    int i;
    return i;
}

int main() {
    f();
    assert(g() == 13);
}

局部变量 -O3

在以下位置进行实施分析:<值优化输出>在gdb中是什么意思?

全局变量

标准:0

实施:.bss部分。

#include <stdio.h>
int i;
int main() {
    printf("%d\n", i);
}

gcc -00 -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部分的中间,该部分从0x6010408个字节开始。

ELF标准,然后保证命名的部分.bss完全填充零的:

.bss本节包含有助于程序存储映像的未初始化数据。根据定义,当程序开始运行时,系统将数据初始化为零。该节不占用文件空间,如节类型所示SHT_NOBITS

此外,该类型SHT_NOBITS是有效的,并且在可执行文件上不占用空间:

sh_size该成员提供节的大小(以字节为单位)。除非段类型为SHT_NOBITS,否则该部分将占用sh_size 文件中的字节。类型的节的SHT_NOBITS大小可能不为零,但在文件中不占空间。

然后由Linux内核在启动程序时将程序加载到内存中时将该内存区域清零。


4

那要看。如果该定义是全局的(在任何函数之外),num则将其初始化为零。如果它是局部的(在函数内部),则其值不确定。从理论上讲,即使尝试读取该值也具有未定义的行为-C允许可能的位不影响该值,但必须以特定的方式进行设置,以使您甚至可以通过读取变量来获得定义的结果。


1

由于计算机的存储容量有限,因此自动变量通常将保存在先前用于其他任意目的的存储元素(无论是寄存器还是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中,并调用foo()。由于x在“ foo”中不需要,并且由于应该将函数的返回值放入寄存器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()使用大于3的模式进行调用将不可避免地导致程序调用未定义行为,因此编译器可以省略仅在mode4或更大时才有意义的任何代码,例如通常可以阻止的代码。在这种情况下发射核武器。请注意,无论标准还是现代的编译器哲学,都不会在乎忽略“嘿”的返回值的事实-尝试返回该值将为编译器提供无限的许可以生成任意代码。


0

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

如果您因此而看到奇怪的行为,则可能取决于声明它的位置。如果在堆栈上的某个函数内,则每次调用该函数时,内容都可能会有所不同。如果是静态作用域或模块作用域,则未定义但不会更改。


0

如果存储类是静态的或全局的,则在加载期间,BSS会将变量或内存位置(ML)初始化为0,除非为变量初始分配了一些值。对于局部未初始化的变量,陷阱表示将分配给内存位置。因此,如果您的任何包含重要信息的寄存器被编译器覆盖,则程序可能会崩溃。

但是某些编译器可能具有避免这种问题的机制。

当我意识到存在陷阱表示时,我正在使用nec v850系列,该陷阱表示具有位模式,这些位模式表示除char之外的数据类型的未定义值。当我使用未初始化的字符时,由于陷阱表示,我得到的默认值为零。这可能对使用necv850es的any1有用


如果使用无符号字符时出现陷阱表示,则说明系统不兼容。明确不允许它们包含陷阱表示,即C17 6.2.6.1/5。
隆丁

-2

num的值将是来自主内存(RAM)的一些垃圾值。如果在创建后立即初始化变量,则更好。


-4

就我所知,它主要取决于编译器,但是在大多数情况下,编译器通常将值假定为0。
在VC ++的情况下,我得到了垃圾值,而TC将值设为0。我将其打印如下

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

例如,如果获得确定性值,则0编译器很可能会采取额外的步骤来确保获得该值(通过添加代码来初始化变量)。一些编译器在进行“调试”编译时会这样做,但是0为它们选择值不是一个好主意,因为它会隐藏代码中的错误(更正确的做法是保证不会出现0xBAADF00D类似的数字)。我认为大多数编译器只会将碰巧占用内存的所有垃圾作为变量的值保留(即,通常不将其赋值为0)。
2016年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.