klu*_*utt 19 c undefined-behavior
我知道诸如x = x++ + ++x调用未定义行为之类的事情,因为变量在同一序列点内被多次修改。这在这篇文章中进行了彻底的解释为什么这些构造使用前增量和后增量未定义行为?
但是考虑像printf("foo") + printf("bar"). 该函数printf返回一个int,因此该表达式在这个意义上是有效的。但是+标准中没有规定运算符的求值顺序,所以不清楚这会打印foobar还是barfoo。
但我的问题是这是否也是未定义的行为。
Eri*_*hil 14
printf("foo") + printf("bar") 没有未定义的行为(下面提到的警告除外),因为函数调用是不确定的,并且不是无序的。
C实际上具有三种排序可能性:
为了区分后两者,假设写入stdout需要将字节放入缓冲区并更新缓冲区中有多少字节的计数器。(为此,我们将忽略缓冲区已满或应该发送到输出设备时发生的情况。)考虑对 的两次写入stdout,称为 A 和 B。
如果 A 和 B 的顺序不确定,那么任何一个都可以先进行,但它的两个部分——写入字节和更新计数器——必须在另一个开始之前完成。如果 A 和 B 是未排序的,则没有什么可以控制这些部分;我们可能有:A 将其字节放入缓冲区,B 将其字节放入缓冲区,A 更新计数器,B 更新计数器。
在前一种情况下,两个写入都已完成,但它们可以按任一顺序完成。在后一种情况下,行为未定义。一种可能性是 B 将其字节写入缓冲区中与 A 字节相同的位置,从而丢失了 A 的字节,因为计数器未更新以告诉 B 其新字节应该去哪里。
在 中printf("foo") + printf("bar"),写入的stdout顺序不确定。这是因为函数调用提供了分隔副作用的序列点,但我们不知道它们的计算顺序。
C 2018 6.5.2.2 10 告诉我们函数调用引入序列点:
在函数指示符和实际参数的计算之后但在实际调用之前有一个序列点。调用函数(包括其他函数调用)中的每一个在被调用函数体执行之前或之后没有特别排序的评估都相对于被调用函数的执行是不确定的。
因此,如果 C 实现恰好在printf("foo")第二个评估,则在实际调用之前有一个序列点,并且 的评估printf("bar")必须在此之前被排序。相反,如果实现printf("bar")首先评估,则printf("foo")必须在它之前进行排序。因此,尽管不确定,但还是有顺序的。
此外,7.1.4 3 告诉我们:
在库函数返回之前有一个序列点。
因此,这两个函数调用的顺序是不确定的。6.5 2 中关于未排序副作用的规则不适用:
如果对标量对象的副作用相对于对同一标量对象的不同副作用或使用同一标量对象的值进行的值计算而言是未排序的,则行为是未定义的……
(更不用说它stdout不是标量对象的事实。)
C 标准允许将标准库函数实现为类似函数的宏 (C 2018 7.1.4 1) 存在危险。在这种情况下,上面关于序列点的推理可能不适用。程序可以通过封闭括号中的名称,以便它不会作为一个函数宏的调用来处理强制函数调用:(printf)("foo") + (printf)("bar")。
不它不是。
这是未指定的行为