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

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

注意:这个问题在接受采访时被问到。

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不是常量表达式,因此必须具有自动存储function。

  • 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标准没有明确定义这里发生的事情,我认为行为是不确定的(通过省略)。

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

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

从N2176(C17草案):

6.7.9初始化

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

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

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

我认为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]的赋值是副作用,并且表达式的评估顺序相对于彼此不确定地排序。 因此,我认为没有办法诉诸标准并声称特定编译器正确或错误地处理此问题。

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

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

int a[5]={1}赋值为[0] = 1

因此它打印1[0]

例如

 char str[10]={'H','a','i'}; char str[0] = 'H'; char str[1] = 'a'; char str[2] = 'i; 

我试着给这个谜题提供一个简短而简单的答案: 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[2]= 1是一个值为1的表达式,你基本上写int a[5]= { 1 }; (副作用是a[2]也被分配1 )。

按照这个

 int a[3] = {1}; // [1,0,0] 

然后检查一下

 int x=5; int a[3] = {x}; // its like a[3]={5} which results [5,0,0] 

然后检查一下

 int x=5; int a[3] = {x = 6}; // its like a[3]={6} which results [6,0,0] 

那么你应该明白这是怎么回事

 int a[3] = {a[0]++}; // [0,0,0] 

要了解这一点,你需要一步一步走。

  • 首先它将运行[0] ++你的数组将成为[1 0 0]
  • a [0] ++是一个表达式,它的值为0,因为它的第一个值为vas 0
  • 最后你的表达式将变成[3] = {0},它将重写你的数组并将其转换为[0 0 0]

那么你应该明白为什么会这样

 int a[3] = {++a[0]}; // [1,0,0] 

它在[0]上运行++运算符,然后将[0] ++的结果写入1到[0]

我希望这个谜团得到解决。 我也给你一个谜题

 int a[3] = {a[1] = ++a[2] + ++a[2]}; // [3,3,2] 
  • 首先我们分配3个整数,你的数组以[0 0 0]开头
  • 如果你检查它将首先运行++ a [2],所以你的数组变为[0 0 1]
  • 然后它将1推到堆栈,这是++ a [2]的第一个结果
  • 然后它运行第二个++ a [2],它是plus的第二个参数,数组变为[0 0 2]
  • 然后它推动第二个++ a [2]的结果,即2
  • 它运行+ 1 + 2并保存结果3
  • 它运行第二个等于运算符,它是[1] = sum = 3数组的结果变为[0 3 2]。 注意结果仍然是3:int x = a = 3就像int x =(a = 3); 和(a = 3)是值为3的表达式。
  • 最后你的表达式变成了[3] = {3},它的作用很简单,它只会分配你的最后一件事,你会得到[3 3 2]

顺便说一句,它可能会导致编译器与编译器之间的不同,甚至在编译器版本之间! 这是小部件:我们从中学到的东西是什么! 不要做那样的事情

操作顺序。

首先,进行赋值并将赋值评估为:

int a[5] = {1} ,产生以下结果:

1, 0, 0, 0, 0 。 我们得到{1}因为a[2]=1计算结果为true,并且在赋值系列中隐式转换为1。

其次,执行大括号内的表达式,这导致实际上在索引2处为数组赋值1。

您可以通过编译int a[5]={true}; 还有a[5]={a[3]=3}; 并观察结果。

编辑:我在初始化列表中的赋值结果错了,导致整数与分配的整数相同。