printf中的序列点

我在这里读到有一个序列点:

在与输入/输出转换格式说明符相关联的操作之后。 例如,在表达式printf("foo %n %d", &a, 42) ,在打印42之前评估%n之后存在序列点。

但是,当我运行此代码时 :

 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。

因为这个问题是因为基于评论的讨论而被问到的,所以我将提供一些背景信息:

第一条评论 :操作顺序保证是您将参数传递给函数的顺序。 有些人(错误地)认为参数将从右到左进行评估,但根据标准,行为是未定义的。

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

回应那个评论 :感谢你的评论我看到printf可以按任何顺序进行评估,但我理解这是因为printf参数是va_list一部分。 你是说任何函数的参数是以任意顺序执行的吗?

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

第二条评论 :是的,这正是我所说的。 甚至调用int your_function(int a, int b) { return a - b; } 并不保证您传递的表达式将从左到右进行计算。 没有序列点(执行先前评估的所有副作用的点)。 举个例子 。 嵌套调用是一个序列点,所以外部调用传递i+1 (13),并且内部调用的返回值(未定义,在本例中为-1,因为i++i计算结果显示为12,13), 但是不能保证一直如此

这清楚地表明这些类型的构造为所有函数触发UB。


维基百科的困惑

OP引用此:

在与输入/输出转换格式说明符相关联的操作之后。 例如,在表达式printf(“foo%n%d”,&a,42)中,在打印42之前评估%n之后存在序列点。

然后将其应用于他的片段( prinf("%d - %d - %d\n", i, your_function(++i, ++i), i); )预先指定格式说明符作为序列点。
通过说“输入/输出转换格式说明符”所指的是%n说明符。 相应的参数必须是指向无符号整数的指针,并且将为其分配到目前为止打印的字符数。 当然,必须在打印其余参数之前评估%n 。 但是,在其他参数中使用为%n传递的指针仍然很危险:它不是 UB(嗯,它不是,但它可以):

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

在调用函数之前有一个序列点,因此在%n设置&a到正确值之前计算表达式100-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 

正如您所看到的, n printf 内部被重新分配,因此您无法在参数列表中使用其新值(因为有一个序列点)。 如果你希望n “被”重新分配“就地”你基本上期望C跳出函数调用,评估其他参数,然后跳回到调用中。 那是不可能的。 如果要更改unsigned int a = 90; to unsigned int a; ,那么行为是未定义的。


关于12年代

现在因为OP读取了序列点,他正确地注意到这句话:

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

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

在函数调用中输入函数之前。 未指定参数的计算顺序,但此序列点表示在输入函数之前所有副作用都已完成

这意味着,在调用printf之前,必须调用your_function (因为它的返回值是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 

虽然后者的输出极不可能(效率非常低)

我想你误解了有关printf序列点(SP)的文字。 它们在某种程度上是exception的,并且只有%n因为这种格式说明符有副作用,并且需要对这些副作用进行排序。

无论如何,在执行printf()以及在评估所有参数之后都有一个SP。 那些格式说明符SP都之后,所以它们不会影响你的问题。

在您的示例中, i的使用都在函数参数中,并且它们都不是用序列点分隔的。 由于您修改了值(两次)并使用该值而没有插入序列点,因此您的代码为UB。

printf关于SP的规则是什么,这个代码forms良好:

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

即使x的值被修改两次。

但是这段代码是UB:

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

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

通过关于评估顺序和UB的C规则,对这个问题的明确答案得到了很大的影响(甚至被阻止)。

评估顺序的具体规则如下:

C99第6.7.9节,第23页:23初始化列表表达式的评估相对于彼此不确定地排序,因此未指定任何副作用发生的顺序。

并且, 此函数调用将显示未定义的行为

 your_function(++i, ++i) 

由于UB,加上评估顺序的规则,准确预测以下的预期结果:

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

是不可能的。

编辑
……我不是在问我为什么我的中期是0.我在问为什么其他两个术语都是12。

无法保证首先调用上述函数的三个参数中的哪一个。 (因为C的评估顺序规则)。 如果首先评估中间函数,那么此时您已调用未定义行为 。 谁能真正说出为什么其他两个条款都是12? 因为在评估第二个参数时我发生的事情是任何人的猜测。