如果在C中我写:

int num;


在给num赋值之前,num的值是否不确定?

评论

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

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

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

#1 楼

静态变量(文件作用域和函数静态)初始化为零:
非静态变量(局部变量)不确定。在分配值之前先读取它们会导致未定义的行为。
int x; // zero
int y = 0; // also zero

void foo() {
    static int x; // also zero
}

在实践中,它们最初在其中往往只具有一些荒谬的值-一些编译器甚至可能放入特定的固定值以使其明显在调试器中查找时-但严格来说,编译器可以从崩溃到通过鼻道召唤恶魔的任何操作。
为什么它是未定义行为而不是简单的“未定义/任意值”,在各种类型的表示中具有附加标志位的CPU体系结构的数量。一个现代的例子是Itanium,它的寄存器中有“ Not Thing”位。当然,C标准起草者正在考虑一些较旧的体系结构。
尝试使用设置了这些标志位的值可能会导致CPU异常,而该异常实际上不会失败(例如,整数加法或分配给另一个变量)。而且,如果您保留未初始化的变量,则编译器可能会在设置了这些标志位的情况下拾取一些随机垃圾,这意味着触摸未初始化的变量可能是致命的。

评论


哦,不,不是。如果您很幸运,那么在调试模式下,当您不在客户面前时,它们可能会出现R的月份。

–马丁·贝克特(Martin Beckett)
09-10-20在21:39

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

–布多伦
09年10月20日在21:43

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

–user172783
09-10-20在21:55

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

– Pavel Minaev
09-10-20在21:59

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

– DigitalRoss
09年10月20日在22:01

#2 楼

如果是静态或全局,则为0;如果存储类为auto,则不确定。
C始终非常明确地声明对象的初始值。如果为global或static,它们将被清零。如果auto,则该值不确定。

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

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


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


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


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



确切不确定的是什么意思是,对于C89,我不确定,C99表示:<7.2不确定值或不确定值或陷阱
表示形式


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

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

评论


通常不确定(用于?)表示它可以执行任何操作。它可以是零,可以是其中的值,它可以使程序崩溃,还可以使计算机从CD插槽中生产蓝莓煎饼。您绝对没有任何保证。它可能导致地球的毁灭。至少就规范而言...制作了实际上执行类似操作的编译器的任何人都会对B-感到不满意

–布莱恩·波斯托(Brian Postow)
09-10-20在22:02

在C11 N1570草案中,不确定值的定义可以在3.19.2中找到。

–user3528438
16-2-26在22:40

是否总是依赖于编译器或操作系统为其设置的静态变量值是多少?例如,如果有人编写了我自己的OS或编译器,并且如果他们还默认将statics的初始值设置为不确定,那可能吗?

– Aditya Singh
17年1月30日在17:41

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

– DigitalRoss
17年1月30日在19:23



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

–伦丁
19-09-12在6:56



#3 楼

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

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

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


不保证变量ab会收到相同的值。有趣的是,这不是一些古怪的理论概念,实际上是优化的结果。

因此,通常来说,“使用内存中的任何垃圾初始化”的流行答案甚至都没有。遥不可及。未初始化的变量的行为与使用垃圾进行初始化的变量的行为不同。

评论


我不明白(很好,我可以),为什么与:D之后一分钟的DigitalRoss相比,它的投票少得多

–安蒂·哈帕拉(Antti Haapala)
17年8月8日在23:04

#4 楼

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


从我们对x86-64调用约定的了解:


%rdi是第一个printf参数,因此地址"%d\n"处的字符串0x4005e4是第二个printf参数,因此是%rsi。来自i,它是第一个4字节的局部变量。

此时,-0x4(%rbp)在堆栈的第一页中已由内核分配,因此要了解该值,我们会研究内核代码并找出将其设置为什么。

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


然后我们还可以进行自己的堆栈修改并编写有趣的内容,例如:

0000000000400536 <main>:
  400536:       55                      push   %rbp
  400537:       48 89 e5                mov    %rsp,%rbp
  40053a:       48 83 ec 10             sub    
#include <assert.h>

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

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

int main() {
    f();
    assert(g() == 13);
}
x10,%rsp 40053e: 8b 45 fc mov -0x4(%rbp),%eax 400541: 89 c6 mov %eax,%esi 400543: bf e4 05 40 00 mov
#include <stdio.h>
int i;
int main() {
    printf("%d\n", i);
}

gcc -00 -std=c99 a.c
x4005e4,%edi 400548: b8 00 00 00 00 mov
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    
readelf -SW a.out
x4005e4,%edi 400547: b8 00 00 00 00 mov
[25] .bss              NOBITS          0000000000601040 001040 000008 00  WA  0   0  4
x0,%eax 40054c: e8 bf fe ff ff callq 400410 <printf@plt> 400551: b8 00 00 00 00 mov q4312078qx0,%eax 400556: 5d pop %rbp 400557: c3 retq 400558: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1) 40055f: 00
x0,%eax 40054d: e8 be fe ff ff callq 400410 <printf@plt> 400552: b8 00 00 00 00 mov q4312078qx0,%eax 400557: c9 leaveq 400558: c3 retq


rbp中的局部变量

实施分析:<值优化输出>在gdb中是什么意思?

全局变量

标准:0

实现:-O3部分。

q4312078q

编译为:

q4312078q

.bss表示# 601044 <i>位于地址i上,并且:

q4312078q

包含:

q4312078q

表示0x601044正好位于0x601044部分的中间,该部分从.bss开始,长8个字节。

然后ELF标准保证名为0x601040的部分完全用零填充:


.bss本节包含未初始化的数据,这些数据会构成程序的内存映像。根据定义,程序开始运行时,系统会使用零初始化
数据。如节类型.bss所示,该节occu-
没有文件空间。


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


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


然后由Linux内核将其归零启动程序将程序加载到内存时的内存区域。

#5 楼

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

#6 楼

由于计算机的存储容量有限,因此自动变量通常将保存在先前用于其他任意目的的存储元素(无论是寄存器还是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()。由于在
“ foo”中不需要x,并且由于应该将函数的返回值放入
寄存器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()不可避免地会导致程序调用Undefined
行为,因此编译器可以忽略仅与代码相关的任何代码
mode为4或更大,例如在这种情况下通常可以防止核发射的代码。请注意,无论是Standard还是
现代的编译器原理,都不会在乎忽略“嘿”的返回值
的事实-尝试将其返回的操作会给出编译器
无限许可以生成任意代码。

#7 楼

基本答案是,是的,它是不确定的。

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

#8 楼

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

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

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

评论


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

–伦丁
19年9月12日在7:00

#9 楼

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

#10 楼

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

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


评论


如果您获得确定性值(例如0),则编译器很可能会采取额外的步骤来确保获得该值(通过添加代码来初始化变量)。一些编译器在进行“调试”编译时执行此操作,但是为它们选择值0是一个坏主意,因为它会隐藏代码中的错误(更正确的做法是保证像0xBAADF00D之类的不太可能的数字)。我认为大多数编译器只会将碰巧占用内存的所有垃圾作为变量的值保留(即,通常不将其赋值为0)。

–天空
16-4-7在7:25