关于C中的数组初始化的困惑


102

在C语言中,如果初始化这样的数组:

int a[5] = {1,2};

那么所有未显式初始化的数组元素将被隐式初始化为零。

但是,如果我这样初始化一个数组:

int a[5]={a[2]=1};

printf("%d %d %d %d %d\n", a[0], a[1],a[2], a[3], a[4]);

输出:

1 0 1 0 0

我不明白,为什么要a[0]打印1而不是0?它是不确定的行为吗?

注意:这个问题是在采访中提出的。


35
该表达式的a[2]=1计算结果为1
tkausl

14
一个非常深刻的问题。我想知道面试官是否知道答案。我不。实际上,表达式的值实际上a[2] = 11,但是我不确定是否允许您将指定的初始化程序表达式的结果用作第一个元素的值。您添加了律师标签的事实意味着我认为我们需要引用标准的答案。
Bathsheba

15
好吧,如果这是他们最喜欢的问题,那么您可能已经躲过了。就我个人而言,我宁愿花几个小时来完成书面编程练习(可以使用编译器和调试器),而不是像上面这样的“ ace”风格问题。我可以想出一个答案,但我认为它没有任何实际事实依据。
Bathsheba

1
@Bathsheba我会做相反的事情,因为这里的答案现在回答了两个问题。
再见SE

1
@Bathsheba将是最好的。在他提出这个话题时,我仍然要把这个问题归功于OP。但这不是我自己决定我认为将是“正确的事情”的原因。
再见SE

Answers:


95

TL; DR:我认为int a[5]={a[2]=1};,至少在C99中,行为不明确。

有趣的是,对我来说唯一有意义的部分就是您要询问的部分:a[0]设置1为该值,因为赋值运算符返回已赋值。其他所有不清楚的地方。

如果有代码int a[5] = { [2] = 1 },一切都会变得很容易:这是一个指定的初始值设定a[2]1其他都是0。但是{ a[2] = 1 }有了一个包含赋值表达式的非指定初始化程序,我们就陷入了困境。


到目前为止,这是我发现的内容:

  • a 必须是局部变量。

    6.7.8初始化

    1. 具有静态存储持续时间的对象的初始化程序中的所有表达式应为常量表达式或字符串文字。

    a[2] = 1不是常量表达式,因此a必须具有自动存储功能。

  • a 在其自身的初始化范围内。

    6.2.1标识符范围

    1. 结构,联合和枚举标签的作用域始于在声明标签的类型说明符中出现标签之后。每个枚举常量的作用域都在其定义的枚举器出现在枚举器列表之后立即开始。任何其他标识符的作用域都在其声明符完成之后立即开始。

    声明符为a[5],因此变量在其自身的初始化范围内。

  • a 在自己的初始化中还活着。

    6.2.4对象的存储期限

    1. 声明其标识符没有链接且没有存储类说明符的对象static具有自动存储期限

    2. 对于不具有可变长度数组类型的对象,其生存期从进入与之关联的块开始一直到该块的执行以任何方式结束。(进入封闭的块或调用函数会挂起,但不会结束当前块的执行。)如果递归地输入该块,则每次都会创建该对象的新实例。对象的初始值不确定。如果为该对象指定了初始化,则每次在执行块时到达声明时都将执行该初始化。否则,每次到达声明时,该值将变得不确定。

  • 之后有一个序列点a[2]=1

    6.8语句和块

    1. 充分表达是不是另一个表达式或一声明符的一部分的表达。下列各是一个完整的表达:一个初始化 ; 表达式语句中的表达式;选择语句(ifswitch)的控制表达式;whileor do语句的控制表达式;for语句的每个(可选)表达式;return语句中的(可选)表达式。完整表达式的结尾是一个序列点。

    请注意,例如,在int foo[] = { 1, 2, 3 }{ 1, 2, 3 }部分中是用括号括起来的初始化程序列表,每个初始化程序后都有一个序列点。

  • 初始化以初始化列表的顺序执行。

    6.7.8初始化

    1. 每个用大括号括起来的初始化器列表都有一个关联的当前对象。如果没有指定,则根据当前对象的类型按顺序初始化当前对象的子对象:下标顺序递增的数组元素,声明顺序的结构成员以及并集的第一个命名成员。[...]

     

    1. 初始化应按初始化器列表顺序进行,为特定子对象提供的每个初始化器都将覆盖先前为同一子对象列出的所有初始化器;所有未明确初始化的子对象都应与具有静态存储持续时间的对象隐式初始化。
  • 但是,初始化表达式不一定按顺序求值。

    6.7.8初始化

    1. 未指定初始化列表表达式中任何副作用发生的顺序。

但是,这仍然有一些问题无法解答:

  • 序列点是否相关?基本规则是:

    6.5表达式

    1. 在上一个序列点与下一个序列点之间,对象的存储值最多只能通过对表达式的求值来修改。此外,先验值应只读以确定要存储的值。

    a[2] = 1 是一个表达式,但初始化不是。

    这与附件J有点矛盾:

    J.2未定义行为

    • 在两个序列点之间,一个对象被修改一次以上,或者被修改,并且除了确定要存储的值之外,还读取先前的值(6.5)。

    附件J列出了所有修饰计数,而不仅仅是表达式修饰。但是鉴于附件是非规范性的,我们可能会忽略这一点。

  • 子对象初始化如何针对初始化表达式排序?是否首先评估所有初始化程序(以某种顺序),然​​后用结果初始化子对象(以初始化程序列表顺序)?还是可以将它们交错?


我认为int a[5] = { a[2] = 1 }执行如下:

  1. a当输入for的存储块时,将为其分配存储空间。此时的内容不确定。
  2. (仅)初始化程序被执行(a[2] = 1),后跟序列点。此店1a[2]和回报1
  3. 1用于初始化a[0](第一个初始化器初始化第一个子对象)。

但在这里事情变得模糊,因为剩余的元素(a[1]a[2]a[3]a[4]),都应该被初始化0,但是当它目前还不清楚:是否之前发生的a[2] = 1评价?如果是这样,a[2] = 1“ win”和overwrite a[2]会不会被覆盖,但是该赋值是否具有未定义的行为,因为在零初始化和赋值表达式之间没有序列点?序列点是否相关(请参见上文)?还是在对所有初始化程序求值后进行零初始化?如果是这样,a[2]最终应该是0

因为C标准没有明确定义此处发生的情况,所以我认为行为是不确定的(忽略)。


1
我会说它是未指定的,而不是未定义的,这使实现可以进行解释。
一些程序员哥们

1
“我们掉进兔子洞了”哈哈!从来没有听说过UB或未指定的东西。
BЈовић

2
@Someprogrammerdude我不认为它是无法指定的(“ 该国际标准提供两种或多种可能性,并且在任何情况下都不会对所选择的标准施加任何要求的行为 ”),因为该标准并未真正提供任何可能性。选择。它只是不说会发生什么,我相信属于“ 由于未明确定义任何行为而在本国际标准中指出了未定义的行为。
melpomene

2
@BЈовић这也是一个非常好的描述,不仅针对未定义的行为,而且还针对需要像这样的线程进行解释的已定义行为。
gnasher729

1
@JohnBollinger的区别在于,您不能a[0]在评估子对象的初始化器之前实际对其进行初始化,并且评估任何初始化器都包括一个序列点(因为它是“完整表达式”)。因此,我相信修改我们要初始化的子对象是公平的游戏。
melpomene

22

我不明白,为什么要a[0]打印1而不是0

大概先a[2]=1初始化a[2],然后将表达式的结果用于initialize a[0]

来自N2176(C17草案):

6.7.9初始化

  1. 初始化列表表达式的求值相对于彼此不确定地排序,因此未指定发生任何副作用的顺序。 154)

因此,似乎输出1 0 0 0 0也是可能的。

结论:不要编写动态修改初始化变量的初始化程序。


1
那部分不适用:这里只有一个初始化程序表达式,因此不需要用任何东西排序。
melpomene 18/09/13

@melpomene还有就是{...}它初始化表达a[2]0a[2]=1其中初始化子表达式a[2]1
user694733 '18

1
{...}是一个初始化的初始化列表。它不是表达。
melpomene

@melpomene好的,您可能就在那里。但是我仍然认为仍有2个相互竞争的副作用,因此,该段仍然有效。
user694733 '18

@melpomene有两件事要排序:第一个初始化程序,以及其他元素的设置为0
MM

6

我认为C11标准涵盖了这种行为,并表示结果未指定,而且我认为C18在此方面没有进行任何相关更改。

标准语言不容易解析。该标准的相关部分为 §6.7.9初始化。语法记录为:

initializer:
                assignment-expression
                { initializer-list }
                { initializer-list , }
initializer-list:
                designationopt initializer
                initializer-list , designationopt initializer
designation:
                designator-list =
designator-list:
                designator
                designator-list designator
designator:
                [ constant-expression ]
                . identifier

请注意,术语之一是Assignment-expression,并且由于a[2] = 1它无疑是一个赋值表达式,因此对于具有非静态持续时间的数组,它可以在初始化器中使用:

§4对于具有静态或线程存储持续时间的对象,初始化器中的所有表达式都应为常量表达式或字符串文字。

关键段落之一是:

§19初始化应按初始化器列表顺序进行,为特定子对象提供的每个初始化器都将覆盖先前为该子对象列出的所有初始化器;151) 所有未明确初始化的子对象应与具有静态存储持续时间的对象隐式初始化。

151)被覆盖的子对象的任何初始化程序,因此不用于初始化该子对象的初始化程序可能根本不会被评估。

另一个关键段落是:

§23初始化列表表达式的求值相对于彼此是不确定的,因此未指定发生任何副作用的顺序。152)

152)特别地,评估顺序不必与子对象初始化的顺序相同。

我很确定第23段指出了问题中的符号:

int a[5] = { a[2] = 1 };

导致未指明的行为。对的赋值a[2]是一种副作用,并且表达式的求值顺序不确定地相对于彼此排序。因此,我认为没有一种方法可以诉诸该标准,并声称某个特定的编译器正在正确或不正确地处理此问题。


初始化列表表达式只有一个,因此§23不相关。
melpomene

2

我的理解是 a[2]=1返回值1,因此代码变为

int a[5]={a[2]=1} --> int a[5]={1}

int a[5]={1}a [0] = 1分配值

因此,它为a [0]打印1

例如

char str[10]={‘H’,‘a’,‘i’};


char str[0] = H’;
char str[1] = a’;
char str[2] = i;

2
这是一个[语言-律师]问题,但这不是与标准兼容的答案,因此使其与标准无关。另外,还有2个更深入的答案可用,您的答案似乎没有添加任何内容。
再见SE

我有一个疑问,我发布的概念是否错误,您能用这个澄清一下吗?
Karthika '18

1
您只是出于某种原因进行推测,而该标准的相关部分已经给出了很好的答案。只是说这将如何发生并不是问题的所在。这与标准所说的应该发生的事情有关。
再见SE

但是张贴以上问题的人问原因,为什么会发生?所以只有我放弃了这个答案,但是概念是正确的,对吗?
卡尔西卡

OP询问“ 这是不确定的行为吗? ”。你的答案没有说。
melpomene 18/09/14

1

我尝试给出一个简短而简单的答案: int a[5] = { a[2] = 1 };

  1. 首先a[2] = 1设置。那意味着数组说:0 0 1 0 0
  2. 但是请注意,假设您已在{ }括号中按顺序进行了初始化,该括号用于按顺序初始化数组,则它将采用第一个值(即1)并将其设置为a[0]。好像int a[5] = { a[2] };将要保留,我们已经到达了a[2] = 1。现在得到的数组是:1 0 1 0 0

另一个示例:int a[6] = { a[3] = 1, a[4] = 2, a[5] = 3 };-尽管顺序在某种程度上是任意的,但假设顺序是从左到右,则将按照以下6个步骤进行:

0 0 0 1 0 0
1 0 0 1 0 0
1 0 0 1 2 0
1 2 0 1 2 0
1 2 0 1 2 3
1 2 3 1 2 3

1
A = B = C = 5不是声明(或初始化)。这是一个正则表达式,其解析为A = (B = (C = 5))因为=运算符是正确的关联。这实际上并不能解释初始化的工作原理。输入定义的块时,该数组实际上开始存在,这可能要比执行实际定义早很多。
melpomene 18/09/13

1
它从左到右,每个以内部声明开头 ”是不正确的。C标准明确表示“ 未指定初始化列表表达式中任何副作用的发生顺序。
melpomene

1
您已经足够多次测试示例中的代码,并查看结果是否一致。 ”这不是它的工作原理。您似乎不了解什么是未定义的行为。默认情况下,C语言中的所有内容都具有未定义的行为;只是某些部分具有标准定义的行为。为了证明某物已经定义了行为,您必须引用标准并显示它定义了应该发生的情况。在没有这样的定义的情况下,行为是不确定的。
melpomene 18/09/14

1
在点(1)的声明是对这里关键问题的一个巨大飞跃:在a[2] = 1应用初始化程序表达式的副作用之前,是否将元素a [2]隐式初始化为0 ?观察到的结果似乎是这样,但是该标准似乎并未指定应该如此。 是争议的中心,这个答案完全忽略了它。
John Bollinger

1
“未定义的行为”是一个狭义的技术术语。这并不意味着“我们不确定的行为”。这里的关键见解是,没有测试,没有编译器,就无法表明某个特定程序是否符合标准,因为如果程序具有未定义的行为,则允许编译器执行任何操作 -包括工作以完全可预测和合理的方式。编译器作者记录事物的不仅仅是实现质量问题,这是未指定或实现定义的行为。
Jeroen Mostert

0

赋值a[2]= 1是一个具有值的表达式,1您实际上已编写 了该函数int a[5]= { 1 };(也具有a[2]赋值的副作用1)。


但是尚不清楚何时评估副作用,并且行为可能会根据编译器而改变。同样,该标准似乎声明这是未定义的行为,因此对编译器特定的实现进行解释无济于事。
再见SE

@KamiKaze:当然,值1偶然落在了那里。
伊夫·达乌斯特

0

我相信,这int a[5]={ a[2]=1 };是程序员将自己射入自己的脚的一个很好的例子。

我可能会觉得您的意思是int a[5]={ [2]=1 };将C99指定的初始化程序设置元素2设置为1,将其余元素设置为0。

在您真正真正想要的罕见情况下,int a[5]={ 1 }; a[2]=1;那将是一种有趣的编写方式。无论如何,这就是您的代码归结为的内容,即使此处有人指出,在a[2]实际执行写入操作时并没有很好地定义它。这里的陷阱是它a[2]=1不是一个指定的初始化程序,而是一个简单的赋值本身,赋值为1。


看起来这个语言律师主题正在要求标准草稿提供参考。这就是为什么你被否决了(我没有这么做,因为你看到我出于同样的原因而被否决了)。我认为您写的内容完全没问题,但看起来这里所有这些语言律师要么来自委托人,要么来自类似人员。因此,他们根本没有寻求帮助,而是试图检查草稿是否涵盖了案件,如果您像帮助他们那样回答,就会触发这里的大多数人。我想我会删掉我的答案:)如果该主题规则明确提出,那将是有帮助的
Abdurrahim
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.