数组批量赋值 - 关于C中数组初始化的困惑



char数组初始化 (5)

在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 ? 是不确定的行为?

注意: 这个问题是在接受采访时提出的。


Answer #1

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

推测 a[2]=1 初始化 a[2] ,表达式的结果用于初始化 a[0]

从N2176(C17草案):

6.7.9初始化

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

所以似乎输出 1 0 0 0 0 也是可能的。

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


Answer #2

TL; DR:我不认为 int a[5]={a[2]=1}; 的行为 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 在其自己的初始化范围内。

    6.2.1标识符的范围

    1. 结构,联合和枚举标记具有在声明标记的类型说明符中标记出现之后开始的范围。 每个枚举常量都具有在枚举器列表中定义枚举器出现之后开始的范围。 任何其他标识符的范围都在其声明者完成之后开始。

    声明符是 a[5] ,因此变量在它们自己的初始化范围内。

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

    6.2.4对象的存储持续时间

    1. 声明标识符没有链接且没有存储类说明符 static 具有 自动存储持续时间

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

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

    6.8声明和块

    1. 完整表达式 是不属于另一个表达式或声明符的表达式。 以下每个都是完整表达式: 初始化器 ; 表达式中的表达式; 选择语句的控制表达式( ifswitch ); whiledo 语句的控制表达式; for 语句的每个(可选)表达式; return 语句中的(可选)表达式。 完整表达式的结尾是序列点。

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

  • 初始化在初始化列表顺序中执行。

    6.7.8初始化

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

    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 ,但是不清楚何时:它是否发生在 a[2] = 1 评估 a[2] = 1 ? 如果是这样, a[2] = 1 将“赢”并覆盖 a[2] ,但是该赋值是否具有未定义的行为,因为零初始化和赋值表达式之间没有序列点? 序列点是否相关(见上文)? 或者在评估所有初始化程序后是否进行零初始化? 如果是这样, a[2] 应该最终为 0

因为C标准没有明确定义这里发生的事情,我认为行为是未定义的(通过省略)。


Answer #3

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

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

在极少数情况下,你真的真的意味着 int a[5]={ 1 }; a[2]=1; int a[5]={ 1 }; a[2]=1; 那么这将是一种有趣的写作方式。 无论如何,这是你的代码归结为,即使有些人在这里指出,当实际执行对 a[2] 的写入时,它没有很好地定义。 这里的缺陷是 a[2]=1 不是指定的初始化器,而是一个本身具有值1的简单赋值。


Answer #4

我认为C11标准涵盖了这种行为,并说结果 没有说明 ,我不认为C18在这方面做了任何相关的改变。

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

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

请注意,其中一个术语是 赋值表达式 ,并且因为 a[2] = 1 无疑是一个赋值表达式,所以允许在具有非静态持续时间的数组的初始化器内:

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

其中一个关键段落是:

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

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

另一个关键段落是:

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

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

我很确定段落§23表明问题中的符号:

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

导致未指明的行为。 对 a[2] 的赋值是副作用,并且表达式的评估顺序相对于彼此不确定地排序。 因此,我认为没有办法诉诸标准并声称特定编译器正确或错误地处理此问题。


Answer #5

赋值 a[2]= 1 是一个值为 1 的表达式,你基本上写 int a[5]= { 1 }; (副作用是 a[2] 也被分配 1 )。





language-lawyer