在C语言中,定义int a = 0, b = a++, c = a++的行为是否定义良好?

33

在C语言中,定义int a = 0, b = a++, c = a++;是否具有定义行为?

或者几乎等效地说,在对象定义中的逗号是否引入了一个序列点,就像表达式中的逗号运算符一样?

类似的问题也被提出过关于C++:

C++的广泛接受的答案是根据C++11标准第8/3段的规定:是的,它是完全定义的

在声明中,每个init-declarator都被单独分析,就好像它是一个独立的声明一样

虽然这段话只涉及到语法分析阶段,并且对于运行时操作的顺序并不十分精确。

C语言的情况如何?C标准是否定义了行为?

类似的问题以前也有人问过:

在声明多个对象的声明中,逗号是否引入类似逗号运算符的序列点?

然而,答案似乎特指C11草案,可能不适用于更近期的C标准版本,因为信息性附录C的措辞自C11草案以来已经发生变化,并且与标准文本似乎不完全一致。

编辑:当然,这样的初始化器似乎没什么意义。我绝对不赞成这种编程风格和结构。这个问题起源于关于一个微不足道的定义的讨论:int res = 0, a = res;,其行为似乎没有完全定义!带有副作用的初始化器并不那么常见,例如考虑这个例子:int arg1 = pop(), arg2 = pop();


2
重复的问题只在C11中得到了回答。相应的条款在C17中已经改变了。C17中的附录C仍然将“不是复合字面量的初始化器”列为引入序列点的完整表达式,但尽管给出了第6.7.9节作为参考,我仍然找不到该节中支持性的规范条款。 - nielsen
1
在参考的重复问题中,被接受的答案包含一些关于用于断言答案的源信息的歧义。在评论中,被接受答案的作者说:“我引用了C11,因为在我写答案时C17还没有最终确定。你是对的,C17和C23(WG14文档日志中的N2176和N3054)似乎都没有包括句子“完整声明符的末尾是一个序列点”。如果您愿意扩展此问题以明确包含更新的标准,我将删除关闭标签。请@我。 - ryyker
1
不客气。考虑到之前链接的重复问题的年代以及C11和C17之间时间上的差距(尚未批准),扩展问题是个明智的举动,显然在那个时候声明符中存在一些序列点方面的缺陷。 - ryyker
1
@ChristosLytras 这个问题已经讨论过了,而且问题本身包含了一个指向这个问题的链接(请参见“编辑”下面的段落)。 - nielsen
2
@ChristosLytras:不,它并没有。你所提到的问题只是部分地涉及了这个问题,并且答案的论点是不完整和不一致的。这就是为什么我提出了一个更精确的问题,得到了两个有详细文献支持的答案,并在不到48小时内获得了超过2000次的浏览。 - chqrlie
显示剩余12条评论
3个回答

18
在C语言中,定义int a = 0, b = a++, c = a++;是否具有定义行为?
是的,因为C 2018 6.8 3条款指出这些初始化(并非全部,见底部)按照它们出现的顺序进行评估:
……具有自动存储期限的对象的初始化器以及具有块作用域的普通标识符的变长数组声明符,在每次达到声明时按照执行顺序进行评估,并将值存储在对象中(包括在没有初始化器的对象中存储不确定值),就好像它是一个语句一样,并且在每个声明中按照声明符出现的顺序进行评估。[强调添加]。
此外,6.8 4条款告诉我们每个初始化器都是一个完整表达式,并且在评估完整表达式和下一个表达式之后存在一个序列点:
一个“完整表达式”是指不属于另一个表达式,也不属于声明符或抽象声明符的表达式。在可变修改类型的非常量大小表达式中,还存在一个隐含的完整表达式,在该完整表达式中,不同大小的表达式的评估与彼此之间的顺序无关。在一个完整表达式的评估和下一个要评估的完整表达式的评估之间存在一个序列点。
根据上述内容,初始化器按照它们出现的顺序进行排序。首先初始化a,因此当对b求值时,a++已经有了一个值,并且与此相关的副作用在c的a++开始之前已经完成,因此整个声明在6.5 2中的“未排序效果”规则下是安全的。
6.8 3存在两个问题:
  • 初始化程序不是语法标记“声明符”的一部分(它们是“初始化声明符”的一部分,是“声明符”的包含标记)。然而,这似乎是一个措辞问题,我们可以将初始化程序视为与其声明符相关联。
  • 它没有指定声明符中表达式的排序(例如,变长数组的大小)和其初始化值之间的顺序。

还要注意,并非所有初始化程序都按照声明中出现的顺序进行评估。6.7.9 23讨论了聚合体和联合体的初始化程序,并表示:

初始化列表表达式的评估在彼此之间具有不确定的顺序,因此任何副作用发生的顺序是未指定的。

历史

上述引用的6.8 3中的措辞可以追溯到C 1999。在C 1990中,它在6.6.2中以以下形式出现,该部分关于复合语句:

...具有自动存储期限的对象的初始化程序将被评估并将值存储在翻译单元中它们的声明符的出现顺序。


在我看来,这是正确的答案,因为它回答了问题,而不依赖于标准中的信息性文本。 - nielsen
值得一提的是,6.8-5(作为一个注释)明确指定“不是复合字面量的初始化器”作为一个完整表达式。 - nielsen
它没有明确规定在声明符(例如变长数组的大小)和其初始化器之间的顺序。变长数组不能有初始化器,因此一个造成问题的定义示例需要额外的间接层级(例如:int a = 9, b[100][9], (*p)[a++] = b[a++];)。 - chqrlie
@nielsen: 我同意。答案更简洁,而且引用C标准的内容更相关。 - chqrlie
1
@chqrlie:已更新。它追溯到1990年代初。 - Eric Postpischil
1
很不幸,这里的英文散文含糊不清。几乎可以肯定你所声称的意图是正确的,但是……“初始化器的评估没有特定的顺序”和“值按照它们在翻译单元中声明符出现的顺序存储在对象中”也是一个有效的解析方式。 - Ben Voigt

16
“在C语言中,定义int a = 0, b = a++, c = a++的行为是否有定义?”...
在当前的C标准ISO/IEC9899:2017中,程序执行的相关内容包含在第§5.1.2.3 (3)节中,其中讨论了顺序和副作用。以下是源文本供参考。
根据下面的文本部分总结,声明语句中的初始化器是有序的,保证了声明中的初始化表达式的执行顺序...
 int a = 0, b = a++, c = a++;

描述了一个"......init-declarator-list(初始化声明符列表)是逗号分隔的声明符序列"(第6.7节 声明)
...不会引发未定义行为,甚至无法确定的结果。每个逗号分隔的表达式都保证从左侧开始顺序执行,直到当前表达式的所有评估和副作用都解决和完成,才会向右移动。这样,每个表达式的结果都是完全定义的。

来自§5.1.2.3

在单个线程执行的评估之间,Sequenced before是一种非对称、传递性的成对关系,它在这些评估之间引入了一个偏序关系。给定任意两个评估A和B,如果A在B之前被排序,则A的执行必须在B的执行之前。反之,如果A在B之前被排序,则B在A之后被排序。如果A既不在B之前也不在B之后被排序,则A和B是未排序的。当A在B之前或之后被排序时,A和B的排序是不确定的(但未指定是哪种情况)。在表达式A和B的评估之间存在一个序列点意味着与A相关的每个值计算和副作用都在与B相关的每个值计算和副作用之前被排序。(附录C中提供了序列点的摘要。)
附录C中提供的相关段落:
“以下是5.1.2.3中描述的序列点:”+(3) ...
在评估一个完整表达式和下一个要评估的完整表达式之间。以下是完整表达式:可变类型的完整声明符;不是复合字面量的初始化器(6.7.9);表达式语句中的表达式(6.8.3);选择语句(if或switch)的控制表达式(6.8.4);while或do语句的控制表达式(6.8.5);for语句的每个(可选)表达式(6.8.5.3);return语句中的(可选)表达式(6.8.6.4)。
(我强调)

3
在这个回答中引用的段落中没有提到初始化器按照它们在源代码中出现的顺序进行评估。当文本说“init-declarator-list是一个由逗号分隔的声明符序列”时,这只是意味着它们是源代码的一个序列,并不涉及评估的顺序。 - Eric Postpischil
1
@EricPostpischil,我认为你引用的引文并不能作为答案依赖的根据来说明它们的评估是顺序进行的。段落中引用了附录 C(位于答案底部)中的内容,它说每个完整表达式之间有一个序列点,并且声明符和初始化器都是完整表达式。你引用的引文只是确立了这些东西是声明符(我想也是为了使“之间”有意义,以便可以在它们之间有序列点)。 - Ben
2
@EricPostpischil底部段落不是说它们之间有序列点吗?而上面的段落不是说如果在任意两个评估A和B之间有序列点,那么“与A相关的每个值计算和副作用都在与B相关的每个值计算和副作用之前进行顺序化”吗?我并不是非常擅长阅读C标准语言,但对我来说这似乎相当直接明了。 - Ben
@Ben 问题在于底部段落来自标准中的一个信息性附录。C语言的行为应该由规范性条款完全定义。 - nielsen
3
@ryyker:“你不同意顺序化既包含词法位置,又在这个上下文中(声明符)指定执行顺序吗?”:不,我不同意。那句话只谈论执行顺序,与词法顺序无关。它涉及到 C 程序中的执行顺序,其中包括一些不按照词法顺序发生的事情,比如函数被调用时的函数体求值、函数参数的求值、循环中的求值顺序、聚合体初始化列表中的初始化器的求值等等... - Eric Postpischil
显示剩余14条评论

2
不要做这种事情...这是灾难的配方...不,别做。句号。

2
确实,这个建议比这个陈述是否明确更重要。它的混淆性质以及对如何评估和形式语义的问题意味着应该避免使用它。另请参阅我的这个回答(尽管是针对一个与C++相关的问题)。 - einpoklum
5
这个问题被标记为[语言律师]。当然,这样的事情往往会导致灾难,问题中的定义仅作为说明。我修改了问题以便澄清。 - chqrlie

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接