printf 函数参数之间的序列点;转换之间的顺序点重要吗?

Sequence Points between printf function args; does the sequence point between conversions matter?

我读到here有一个序列点:

After the action associated with input/output conversion format specifier. For example, in the expression printf("foo %n %d", &a, 42), there is a sequence point after the %n is evaluated before printing 42.

然而,当我 运行 this code:

int your_function(int a, int b) {
    return a - b;
}

int main(void) {
    int i = 10;

    printf("%d - %d - %d\n", i, your_function(++i, ++i), i);
}

我得到的不是我期望的:

12 - 0 - 12

意味着不是为转换格式说明符创建的序列点。 http://en.wikipedia.org 是错误的,还是我只是误解了什么,或者在这种情况下 gcc 不兼容(顺便说一句 Visual Studio 2015 产生了同样的意外结果)?

编辑:

我知道 your_function 的参数被评估和分配给参数的顺序是未定义的。我不是在问为什么我的中间项是 0。我是在问为什么其他两个项都是 12。

我认为您误解了有关 printf 序列点 (SP) 的文本。它们在某种程度上是一种异常现象,并且仅与 %n 一起使用,因为此格式说明符具有副作用,并且需要对这些副作用进行排序。

反正在printf()开始执行和所有参数求值后都有一个SP。那些 format-specifier SP 都是 这个之后,所以它们不会影响你的问题。

在你的例子中,i的使用都在函数参数中,其中none用序列点分隔。由于您修改了该值(两次)并在不干预序列点的情况下使用该值,因此您的代码是 UB。

printf中关于SP的规则的意思是这段代码格式正确:

int x;
printf("%d %n %d %n\n", 1, &x, 2, &x);

即使x的值修改了两次

但是这个代码是UB:

int x = 1;
printf("%d %d\n", x, ++x);

注意:请记住,%n 表示到目前为止写入的字符数被复制到相关参数指向的整数。

C 规则对评估顺序和 UB 的强烈影响(甚至阻止)对此问题给出明确的答案。

此处规定了有关评估顺序的特定规则:

C99 section 6.7.9, p23: 23 The evaluations of the initialization list expressions are indeterminately sequenced with respect to one another and thus the order in which any side effects occur is unspecified.

并且,this function call will exhibit undefined behavior:

your_function(++i, ++i)

因为UB,加上评价顺序的规则,对以下预期结果的预测准确:

printf("%d - %d - %d\n", i, your_function(++i, ++i), i);

不可能。

编辑
...我不是在问为什么我的中间项是 0。我是在问为什么其他两个项都是 12。

无法保证首先调用上述函数的三个参数中的哪一个。 (因为 C 的评估顺序规则)。如果首先评估中间函数,那么 在那个点 你已经调用了 Undefined Behavior 。谁能真正说出为什么其他两项是12?。因为当评估第二个参数时 i 会发生什么是任何人的猜测。

因为 comment-based 讨论 提出了这个问题,我将提供一些背景信息:

first comment: The order of operations is not guaranteed to be the order in which you pass arguments to the function. Some people (wrongly) assume that the arguments will be evaluated right to left, but according to the standard, the behaviour is undefined.

OP 接受并理解这一点。没有必要重复 your_function(++i, ++i) 是 UB 的事实。

In response to that comment: Thanks to your comment I see that printf may be evaluated in any order, but I understood that to be because printf arguments are part of a va_list. Are you saying that the arguments to any function are executed in an arbitrary order?

OP 要求说明,所以我详细说明一下:

Second comment: Yes, that's exactly what I'm saying. even calling int your_function(int a, int b) { return a - b; } does not guarantee that the expressions you pass will be evaluated left to right. There's no sequence point (a point at which all side effects of previous evaluations are performed). Take this example. The nested call is a sequence point, so the outer call passes i+1 (13), and the return value of the inner call (undefined, in this case -1 because i++, i evaluates to 12, 13 apparently), but there's no guarantee that this will always be the case

这很清楚这些类型的构造会触发所有功能的 UB。


维基百科混乱

OP 引用这个:

After the action associated with input/output conversion format specifier. For example, in the expression printf("foo %n %d", &a, 42), there is a sequence point after the %n is evaluated before printing 42.

然后将其应用于他的代码段 (prinf("%d - %d - %d\n", i, your_function(++i, ++i), i);),期望格式说明符用作序列点。
"input/output 转换格式说明符" 所指的是 %n 说明符。相应的参数 必须 是指向无符号整数的指针,并且它将被分配到目前为止打印的字符数。自然地, %n 必须在打印其余参数之前进行评估。然而,在其他参数中使用为 %n 传递的指针仍然是危险的:它是 而不是 UB(好吧,它不是,但它可以):

printf("Foo %n %*s\n", &a, 100-a, "Bar");//DANGER!!

在调用函数之前有一个序列点,所以表达式100-a 将在之前计算%n 已将 &a 设置为正确的值。如果 a 未初始化,则 100-a 是 UB。如果a初始化为0,比如的结果会为100。不过总的来说,这样的代码还是比较自找麻烦的。将其视为非常糟糕的做法,或更糟...
只需查看以下任一语句生成的输出:

unsigned int a = 90;
printf("%u %n %*s\n",a,  &a, 10, "Bar");//90         Bar
printf("%u\n", a);//3
printf("Foo %u %n %*s\n",a, &a, 10-a, "Bar");//Foo 3      Bar < padding used: 10 - 3, not 10 - 6 
printf("%u\n", a);//6

如您所见,nprintf 中被重新分配 ,因此您不能在参数列表中使用它的新值 (因为有一个序列点)。如果您期望 n 被重新分配为“in-place”,您实际上是在期望 C 跳出函数调用,评估其他参数,然后跳回调用。那是不可能的。如果您要将 unsigned int a = 90; 更改为 unsigned int a;,则行为未定义。


关于12

现在因为 OP 阅读了序列点,他正确地注意到了这个陈述:

printf("%d - %d - %d\n", i, your_function(++i, ++i), i);

略有不同:your_function(++i, ++i)一个序列点,而保证 i 将递增两次。此函数调用是一个序列点,因为:

Before a function is entered in a function call. The order in which the arguments are evaluated is not specified, but this sequence point means that all of their side effects are complete before the function is entered

这意味着,在调用printf之前,your_function 必须要调用(因为它的return值是printf 调用的参数),并且 i 将递增两次。
可以 解释输出为 "12 - 0 - 12",但是否保证是输出?

没有

从技术上讲,虽然大多数编译器会首先评估 your_function(++i, ++i); 调用,但该标准将允许编译器从左到右评估传递给 sprintf 的参数(毕竟没有指定顺序) ).所以这将是一个同样有效的结果:

10 - 0 - 12
//or even
12 - 0 - 10
//and
10 - 0 - 10
//technically, even this would be valid
12 - 0 - 11

尽管后一种输出极不可能(效率很低)