定义 int a = 0, b = a++, c = a++; 在 C 中定义了行为吗?

chq*_*lie 33 c undefined-behavior sequence-points language-lawyer c17

C 中的定义是否int a = 0, b = a++, c = a++;定义了行为?

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

对于 C++ 也提出了类似的问题:

C++ 被广泛接受的答案是“是”,它在 C++11 标准第 8/3 段中有完整定义:

声明中的每个初始化声明符都会被单独分析,就像它本身在声明中一样

尽管本段仅涉及语法分析阶段,对于运行时的操作顺序还不够精确。

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

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

多个对象声明中的逗号是否会像逗号运算符一样引入序列点?

然而,答案似乎专门针对 C11 草案,并且可能不适用于 C 标准的最新版本,因为自 C11 草案以来,信息性附录 C 的措辞已发生变化,并且似乎也不与标准文本完全一致。

编辑:当然这样的初始化器似乎毫无用处地扭曲。我绝对不会容忍这样的编程风格和结构。这个问题源于关于一个琐碎定义的讨论:int res = 0, a = res;行为似乎没有完全定义(!)。具有副作用的初始化器并不少见,例如考虑以下一个:int arg1 = pop(), arg2 = pop();

Eri*_*hil 18

\n

C 中的定义是否int a = 0, b = a++, c = a++;定义了行为?

\n
\n

是的,因为 C 2018 6.8 3 说这些初始化(不是全部,见底部)按照它们出现的顺序进行评估:

\n
\n

\xe2\x80\xa6 具有自动存储持续时间的对象的初始值设定项以及具有块作用域的普通标识符的可变长度数组声明符将被评估,并将值存储在对象中(包括在没有初始值设定项的对象中存储不确定值) ) 每次按照执行顺序到达声明,就好像它是一个语句,并且在每个声明中按照声明符出现的顺序。[强调。]

\n
\n

另外,6.8 4 告诉我们每个初始值设定项都是一个完整表达式,并且在完整表达式的求值和下一个表达式的求值之后有一个序列点:

\n
\n

完整表达式是不属于另一个表达式的一部分的表达式,也不属于声明符或抽象声明符的一部分。还有一个隐式完整表达式,其中评估可变修改类型的非常量大小表达式;在该完整表达式中,不同大小表达式的求值彼此之间是无序的。在完整表达式的求值和下一个要求值的完整表达式的求值之间存在一个序列点。

\n
\n

考虑到上述两点,初始化器按照它们出现的顺序进行排序。a首先初始化,因此当a++评估 for时有一个值b,并且其副作用在a++forc开始之前完成,因此整个声明对于 6.5 中的 \xe2\x80\x9cnsequencedeffects\xe2\x80\x9d 规则是安全的2.

\n

6.8 3 有点欠缺,原因有二:

\n
    \n
  • 初始化器不是语法标记声明器的一部分(它们是init-declarator的一部分,是declarator的包含标记)。然而,这似乎是一个措辞问题,我们可以将初始化器与其声明器关联起来。
  • \n
  • 它不指定声明符中的表达式(例如可变长度数组的大小)与其初始值设定项之间的顺序。
  • \n
\n

另请注意,并非所有初始值设定项都按照它们在声明中出现的顺序进行计算。6.7.9 23 讨论了聚合和联合的初始值设定项并表示:

\n
\n

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

\n
\n

历史

\n

上面引用的 6.8 3 中的措辞可以追溯到 C 1999。在 C 1990 中,它在 6.6.2 中具有这种形式,它是关于复合语句的:

\n
\n

\xe2\x80\xa6 具有自动存储持续时间的对象的初始值设定项将被评估,并且值按照其声明符在翻译单元中出现的顺序存储在对象中。

\n
\n


ryy*_*ker 16

\n

“定义 int a = 0, b = a++, c = a++; 在 C 中是否定义了行为?”...

\n
\n

在当前的 C 标准 ISO/IEC9899:2017 中,\xc2\xa75.1.2.3 (3) 节介绍了程序执行,其中包括对排序和副作用的讨论。现将原文转载如下,供参考。

\n

从下面的文本部分总结,声明语句中的初始值设定项是排序的,保证声明中的初始值设定项表达式发布...

\n
 int a = 0, b = a++, c = a++;\n
Run Code Online (Sandbox Code Playgroud)\n

它描述了“...init-declarator-list [which]是一个以逗号分隔的声明符序列”(第 6.7 节声明)
\n...不会调用未定义的行为,甚至不会调用不确定的结果。每个逗号分隔的表达式都保证从左侧开始排序,并且不会向右移动,直到当前表达式的所有计算和副作用都得到解析和完成。这样每个表达式的结果就被完全定义了。

\n

来自\xc2\xa75.1.2.3

\n
\n

“排序在前是单个线程执行的评估之间的不对称、传递、成对关系,这导致这些评估之间存在偏序。给定任意两个评估 A 和 B,如果 A 在 B 之前排序,则A 的执行应先于 B 的执行。(相反,如果 A 排序在 B 之前,则 B 排序在 A 之后。)如果 A 未排序在 B 之前或之后,则 A 和 B 不排序。当 A 在 B 之前或之后排序时,\n 和 B 的排序是不确定的,但\n未指定是哪个。13) 表达式 A 和 B 之间\n的序列点的存在意味着每个值计算\n和相关的副作用在与 B 相关的每个值\n计算和副作用之前对 A 进行排序。(附录 C 中给出了\n序列点的摘要。)”

\n
\n

附件 C 中提供的相关段落:

\n

“以下是5.1.2.3中描述的序列点:” + (3)\n...

\n
\n

“在完整表达式的求值和要求值的下一个完整表达式之间。以下是完整表达式:可变修改类型的完整声明符;不属于复合文字的初始化程序 (6.7.9) ; 表达式语句中的表达式\n (6.8.3) ; 选择语句的控制表达式\n(if 或 switch) (6.8.4); while 或 do 语句的控制表达式\n (6.8.5); for 语句的每个(可选)表达式\n(6.8.5.3);return 语句中的(可选)表达式\n(6.8.6.4)"
\n(强调我的)

\n
\n

  • 此答案中引用的段落中没有任何内容表明初始化程序是按照它们在源代码中出现的顺序进行评估的。其中文本说“init-declarator-list 是逗号分隔的声明符序列”,这仅意味着它们是源代码序列。它不涉及评估的顺序。 (3认同)
  • @ryyker:“你不同意排序意味着词汇位置并且在这种情况下(声明符)指定执行顺序吗?”:不,我不同意。这句话只讲执行顺序,与词汇顺序无关。它与整个 C 程序的执行顺序有关,其中包括一些不按词法顺序发生的事情,包括调用函数时对函数体的求值、对函数参数的求值、循环中求值的顺序、在循环中对初始值设定项的求值。聚合列表,等等…… (3认同)
  • @EricPostpischil底部段落不是说它们之间存在序列点吗?上面的段落说如果任何两个评估 A 和 B 之间存在序列点,那么“与 A 相关的每个值计算和副作用都在每个之前排序与 B 相关的值计算和副作用”。我在阅读 C 标准语言方面无论如何都没有超级经验,但这对我来说似乎相当简单。 (2认同)