作者:mobiledu2502899157 | 来源:互联网 | 2022-12-04 23:15
在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
?是不确定的行为?
注意:这个问题是在接受采访时提出的.
1> melpomene..:
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初始化
具有静态存储持续时间的对象的初始化程序中的所有表达式应为常量表达式或字符串文字.
a[2] = 1
不是一个常量表达式,因此a
必须具有自动存储功能.
a
在自己的初始化范围内.
6.2.1标识符的范围
结构,联合和枚举标记具有在声明标记的类型说明符中标记出现之后开始的范围.每个枚举常量都具有在枚举器列表中定义枚举器出现之后开始的范围.任何其他标识符的范围都在其声明者完成之后开始.
声明符是a[5]
,因此变量在它们自己的初始化范围内.
a
在自己的初始化中还活着.
6.2.4对象的存储持续时间
声明标识符没有链接且没有存储类说明符的对象static
具有自动存储持续时间.
对于没有可变长度数组类型的此类对象,其生命周期从entry进入与其关联的块,直到该块的执行以任何方式结束.(输入一个封闭的块或调用一个函数暂停,但不会结束,执行当前块.)如果以递归方式输入块,则每次都会创建一个新的对象实例.对象的初始值是不确定的.如果为对象指定了初始化,则每次在执行块时达到声明时都会执行初始化; 否则,每次达到声明时,该值将变为不确定.
之后有一个序列点a[2]=1
.
6.8声明和块
甲充分表达是不是另一个表达式或一声明符的一部分的表达.以下每个都是完整表达式:初始化器 ; 表达式中的表达式; 选择陈述(if
或switch
)的控制表达; 一个while
或一个do
陈述的控制表达; for
语句中的每个(可选)表达式; return
语句中的(可选)表达式.完整表达式的结尾是序列点.
请注意,例如,在int foo[] = { 1, 2, 3 }
该{ 1, 2, 3 }
部分中是一个括号括起来的初始化器列表,每个初始化器都有一个序列点.
初始化在初始化列表顺序中执行.
6.7.8初始化
每个大括号括起的初始化列表都有一个关联的当前对象.当没有指定时,根据当前对象的类型按顺序初始化当前对象的子对象:增加下标顺序的数组元素,声明顺序中的结构成员,以及union的第一个命名成员.[...]
初始化应在初始化器列表顺序中进行,每个初始化器为特定子对象提供,覆盖同一子对象的任何先前列出的初始化器; 未明确初始化的所有子对象应与具有静态存储持续时间的对象隐式初始化.
但是,初始化表达式不一定按顺序进行评估.
6.7.8初始化
未指定初始化列表表达式中出现任何副作用的顺序.
但是,这仍然有一些问题没有答案:
序列点是否相关?基本规则是:
6.5表达式
在前一个和下一个序列点之间,对象的存储值最多只能通过表达式的计算来修改一次.此外,先前的值应该只读以确定要存储的值.
a[2] = 1
是一个表达式,但初始化不是.
这与附件J略有矛盾:
J.2未定义的行为
在两个序列点之间,对象被多次修改,或者被修改,并且读取先前值而不是确定要存储的值(6.5).
附件J表示任何修改都很重要,而不仅仅是表达式的修改.但鉴于附件是非规范性的,我们可能会忽略这一点.
如何针对初始化表达式对子对象初始化进行排序?是否首先评估所有初始值设定项(按某种顺序),然后使用结果初始化子对象(在初始化列表顺序中)?或者它们可以交错吗?
我认为int a[5] = { a[2] = 1 }
执行如下:
存储for a
在输入其包含块时分配.此时内容是不确定的.
执行(仅)初始化程序(a[2] = 1
),然后执行序列点.该文件存储1
在a[2]
和回报1
.
这1
用于初始化a[0]
(第一个初始化器初始化第一个子对象).
但在这里事情变得模糊,因为剩余的元素(a[1]
,a[2]
,a[3]
,a[4]
),都应该被初始化为0
,但目前还不清楚时:它发生之前a[2] = 1
进行评估?如果是这样,a[2] = 1
将"赢"并覆盖a[2]
,但是该赋值是否具有未定义的行为,因为零初始化和赋值表达式之间没有序列点?序列点是否相关(见上文)?或者在评估所有初始化程序后是否进行零初始化?如果是这样,a[2]
应该最终成为0
.
因为C标准没有明确定义这里发生的事情,我认为行为是未定义的(通过省略).
@Someprogrammerdude我不认为它可以不被指定("*国际标准提供两种或更多种可能性的行为,并且在任何情况下都没有选择任何进一步的要求*")因为该标准实际上没有提供任何可能性哪个可供选择.它只是没有说明发生了什么,我认为这属于"*未定义的行为在本国际标准[...]中通过省略任何明确的行为定义来表明.*"
@BЈовић这也是一个非常好的描述,不仅对于未定义的行为,而且对于需要像这个解释的线程的定义行为.
2> user694733..:
我不明白,为什么a[0]
打印1
而不是0
?
可能首先a[2]=1
初始化a[2]
,表达式的结果用于初始化a[0]
.
从N2176(C17草案):
6.7.9初始化
初始化列表表达式的评估相对于彼此不确定地排序,因此未指定任何副作用发生的顺序. 154)
所以似乎输出1 0 0 0 0
也是可能的.
结论:不要编写初始化程序来动态修改初始化变量.
3> Jonathan Lef..:
我认为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]
是副作用,并且表达式的评估顺序相对于彼此不确定地排序.因此,我认为没有办法诉诸标准并声称特定编译器正确或错误地处理此问题.