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

时间:2018-09-13 05:58:45

标签: c arrays initialization language-lawyer

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

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

7 个答案:

答案 0 :(得分:94)

TL; DR:至少在C99中,我认为int a[5]={a[2]=1};的行为没有得到很好的定义。

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

如果代码是int a[5] = { [2] = 1 },一切将变得很容易:这是将a[2]设置为1的指定初始值设定项,其他设置为0。但是使用{ a[2] = 1 },我们有了一个未指定的初始化器,该初始化器包含一个赋值表达式,并且掉进了一个兔子洞。


这是我到目前为止所发现的:

  • a必须是局部变量。

      

    6.7.8初始化

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

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

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

      

    6.2.1标识符范围

         
        
    1. Structure,union和枚举标签的作用域始于出现   声明标签的类型说明符中的标签。每个枚举常量的范围如下:   在其定义的枚举器出现在枚举器列表之后立即开始。 任何   其他标识符的范围从其声明符完成之后开始。
    2.   

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

  • a仍在其自身的初始化中。

      

    6.2.4对象的存储期限

         
        
    1. 一个其标识符声明为没有链接且没有存储类的对象   指定者static具有自动存储期限

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

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

      

    6.8语句和块

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

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

  • 初始化是按照初始化列表的顺序进行的。

      

    6.7.8初始化

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

         
        
    1. 初始化应以初始化列表的顺序进行,每个初始化提供一个   覆盖同一子对象的所有先前列出的初始化程序的特定子对象;所有   未显式初始化的子对象应隐式初始化为与   具有静态存储期限的对象。
    2.   
  • 但是,初始化表达式不一定按顺序求值。

      

    6.7.8初始化

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

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

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

      

    6.5表达式

         
        
    1. 在上一个和下一个序列点之间,对象应具有其存储值   最多可以通过对表达式的求值进行修改。此外,先验价值   应该只读,以确定要存储的值。
    2.   

    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],但是该赋值是否具有未定义的行为,因为零初始化和赋值表达式之间没有序列点?序列点是否相关(请参见上文)?还是在评估完所有初始化程序后会发生零初始化?如果是这样,a[2]应该最终是0

由于C标准并未明确定义此处发生的情况,因此我认为行为是不确定的(忽略)。

答案 1 :(得分:22)

  

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

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

来自N2176(C17草案):

  

6.7.9初始化

     
      
  1. 初始化列表表达式的求值相对于   彼此并且因此未指定发生副作用的顺序。 154)
  2.   

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

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

答案 2 :(得分:6)

我认为C11标准涵盖了这种行为,并说结果 是未指定,并且我认为C18在 这个区域。

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

  

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

请注意,其中一项是 assignment-expression ,并且由于a[2] = 1无疑是一个赋值表达式,因此允许在内部 具有非静态持续时间的数组的初始化程序:

  

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

关键段落之一是:

  

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

     

151)覆盖子对象的任何初始化程序   因此不用于初始化子对象可能不会在   全部。

另一个关键段落是:

  

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

     

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

我很确定第§23段指出 问题:

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

导致未指定的行为。 对a[2]的赋值是副作用,并且对{{1}}的赋值顺序 表达式彼此之间不确定地排序。 因此,我认为没有办法呼吁该标准, 声称特定的编译器正在正确或不正确地处理此问题。

答案 3 :(得分: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;

答案 4 :(得分:1)

对于这个难题,我尝试给出一个简短的答案:int a[5] = { a[2] = 1 };

  1. 已设置第一个a[2] = 1。这意味着数组显示:0 0 1 0 0
  2. 但是,请注意,假设您在{ }括号中进行过排序,该括号用于按顺序初始化数组,它将使用第一个值(即1)并将其设置为{{ 1}}。好像a[0]会保留在我们已经得到int a[5] = { a[2] };的位置。现在,结果数组为:a[2] = 1

另一个例子:1 0 1 0 0-即使顺序是任意的,假设顺序是从左到右,也可以按照以下6个步骤进行操作:

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

答案 5 :(得分:0)

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

答案 6 :(得分:-1)

我相信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的赋值。