【问题标题】:Confusion about array initialization in C关于 C 中的数组初始化的困惑
【发布时间】:2019-02-17 19:42:05
【问题描述】:

在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?是未定义的行为吗?

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

【问题讨论】:

  • 表达式a[2]=1 的计算结果为1
  • 一个非常深刻的问题。我想知道面试官自己是否知道答案。我不。实际上,表达式a[2] = 1 的值实际上是1,但我不确定是否允许您将指定的初始化表达式的结果作为第一个元素的值。您添加了律师标签的事实意味着我认为我们需要引用标准的答案。
  • 好吧,如果这是他们最喜欢的问题,那么您很可能已经躲过了一劫。就我个人而言,我更喜欢在几个小时内完成书面编程练习(可以访问编译器和调试器),而不是像上面这样的“王牌”风格的问题。我可以推测一个答案,但我认为它没有任何真正的事实依据。
  • @Bathsheba 我会做相反的事情,因为这里的答案现在回答了这两个问题。
  • @Bathsheba 将是最好的。当他提出这个话题时,我仍然会把这个问题归功于 OP。但这不是由我来决定我认为什么是“正确的事情”。

标签: c arrays initialization language-lawyer


【解决方案1】:

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);这 控制whiledo 语句的表达式;的每个(可选)表达式 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 的存储空间在进入其包含块时分配。此时内容不确定。
  2. (唯一的)初始化程序被执行(a[2] = 1),然后是一个序列点。这会将1 存储在a[2] 中并返回1
  3. 那个1用来初始化a[0](第一个初始化器初始化第一个子对象)。

但是这里事情变得模糊了,因为剩余的元素(a[1]a[2]a[3]a[4])应该被初始化为0,但不清楚什么时候:它发生在@之前987654359@被评估了吗?如果是这样,a[2] = 1 将“获胜”并覆盖a[2],但该赋值是否会因为在零初始化和赋值表达式之间没有序列点而具有未定义的行为?序列点是否相关(见上文)?还是在评估所有初始化程序之后发生零初始化?如果是这样,a[2] 最终应该是 0

由于 C 标准没有明确定义此处发生的情况,我认为该行为是未定义的(通过遗漏)。

【讨论】:

  • 我会争辩说它是 unspecified 而不是 undefined,这让实现的解释变得开放。
  • “我们掉进了兔子洞”哈哈!从来没有听说过 UB 或未指定的东西。
  • @Someprogrammerdude 我不认为它可以是未指定的(“本国际标准提供两种或多种可能性的行为,并且在任何情况下都没有对选择哪个施加进一步的要求 ") 因为该标准并没有真正提供任何可供选择的可能性。它根本没有说明会发生什么,我认为这属于“未定义的行为 [...] 在本国际标准中 [...] 通过省略任何明确的行为定义来表示。 "
  • @BЈовић 这也是一个非常好的描述,不仅对于未定义的行为,而且对于需要像这样的线程来解释的已定义行为。
  • @JohnBollinger 不同之处在于您不能在评估其初始化程序之前实际初始化a[0] 子对象,并且评估任何初始化程序都包含一个序列点(因为它是一个“完整表达式”)。因此,我相信修改我们正在初始化的子对象是公平的游戏。
【解决方案2】:

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

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

来自 N2176(C17 草案):

6.7.9 初始化

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

所以看起来输出1 0 0 0 0 也是可能的。

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

【讨论】:

  • 那部分不适用:这里只有一个初始化表达式,所以不需要任何排序。
  • @melpomene 有将a[2] 初始化为0{...} 表达式,以及将a[2] 初始化为1a[2]=1 子表达式。
  • {...} 是一个花括号初始化列表。这不是一个表达式。
  • @melpomene 好的,你可能就在那儿。但我仍然认为仍然存在 2 个相互竞争的副作用,因此该段成立。
  • @melpomene 有两件事要排序:第一个初始化器,以及将其他元素设置为 0
【解决方案3】:

我认为 C11 标准涵盖了这种行为并表示结果 未指定,我认为 C18 没有在 这个区域。

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

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不相关。
【解决方案4】:

我的理解是 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;

【讨论】:

  • 这是一个 [language-lawyer] 问题,但这不是符合标准的答案,因此无关紧要。此外,还有 2 个更深入的答案可用,您的答案似乎没有添加任何内容。
  • 我有一个疑问。我发布的概念有误吗?你能澄清一下吗?
  • 您只是推测原因,而标准的相关部分已经给出了很好的答案。只是说它是如何发生的并不是问题所在。这是关于标准所说的应该发生的事情。
  • 但是发布上述问题的人问了原因以及为什么会发生这种情况?所以只有我放弃了这个答案。但概念是正确的。对吧?
  • OP 询问“这是未定义的行为吗?”。你的回答没有说​​。
【解决方案5】:

我试着给出一个简短的答案: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

【讨论】:

  • A = B = C = 5 不是声明(或初始化)。这是一个解析为A = (B = (C = 5)) 的普通表达式,因为= 运算符是右关联的。这并不能真正帮助解释初始化是如何工作的。当输入定义它的块时,数组实际上开始存在,这可能在实际定义执行之前很久。
  • 从左到右,每个都以内部声明开头”不正确。 C 标准明确规定“未指定初始化列表表达式中任何副作用发生的顺序。
  • "您对我的示例中的代码进行了足够多次的测试,看看结果是否一致。" 这不是它的工作原理。你似乎不明白什么是未定义的行为。默认情况下,C 中的 Everything 具有未定义的行为;只是某些部分具有标准定义的行为。为了证明某事已经定义了行为,您必须引用标准并显示它定义应该发生什么的地方。在没有这样的定义的情况下,行为是未定义的。
  • 第 (1) 点中的断言是对这里关键问题的巨大飞跃:元素 a[2] 到 0 的隐式初始化是否发生在 a[2] = 1 初始化表达式的副作用之前应用?观察到的结果好像是这样,但标准似乎没有规定应该是这种情况。 是争议的中心,这个答案完全忽略了它。
  • “未定义行为”是一个狭义的技术术语。这并不意味着“我们不太确定的行为”。这里的关键见解是,没有编译器的任何测试都无法显示特定程序的行为是否良好根据标准,因为如果程序具有未定义的行为,则允许编译器做任何事情——包括以完全可预测和合理的方式工作。这不仅仅是编译器编写者记录事物的实现质量问题 - 这是未指定或实现定义的行为。
【解决方案6】:

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

【讨论】:

  • 但不清楚何时评估副作用,并且行为可能会根据编译器而改变。此外,该标准似乎声明这是未定义的行为,因此对编译器特定实现的解释没有帮助。
  • @KamiKaze:当然,值 1 是偶然落到那里的。
【解决方案7】:

我相信,int a[5]={ a[2]=1 }; 是程序员开枪打自己脚的好例子。

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

在极少数情况下,你真的是指int a[5]={ 1 }; a[2]=1;,那么这将是一种有趣的写作方式。无论如何,这就是您的代码归结为的内容,尽管这里有些人指出,在实际执行对a[2] 的写入时,它的定义并不明确。这里的陷阱是a[2]=1 不是一个指定的初始化器,而是一个简单的赋值,它本身的值为 1。

【讨论】:

  • 看起来这个语言律师主题是从标准草案中询问参考。这就是您被否决的原因(我没有这样做,因为您看到我出于同样的原因而被否决)。我认为你写的完全没问题,但看起来这里所有的语言律师要么来自委员会,要么来自类似的地方。因此,他们根本没有寻求帮助,他们正在尝试检查草稿是否涵盖此案,并且如果您像帮助他们一样回答问题,这里的大多数人都会被触发。我想我会删除我的答案:) 如果这个主题规则明确说明那会很有帮助
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-06-30
  • 2013-01-10
  • 1970-01-01
  • 1970-01-01
  • 2018-04-25
  • 2021-03-13
  • 2015-01-30
相关资源
最近更新 更多