【问题标题】:Sequence Points between printf function args; does the sequence point between conversions matter?printf 函数参数之间的序列点;转换之间的序列点重要吗?
【发布时间】:2016-04-10 19:32:19
【问题描述】:

我读到here 有一个序列点:

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

但是,当我运行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。

【问题讨论】:

  • 根据this,这是未定义的行为。我也相信your_function(++i, ++i) 也是未定义的行为。
  • your_function(++i, ++i) 显然是 UB。
  • 标准引用所描述的序列点在printf() 函数被调用后位于其主体内。在调用printf() 函数之前,您在调用序列中有极端未定义的行为——这意味着任何结果都是可接受的(包括您得到的结果)。
  • @lurker:两个增量都必须在调用your_function() 之前完成;在评估函数调用的参数之后有一个序列点,因此参数列表中的副作用是完整的。不清楚的是相同的值是否被两次传递给your_function()——它是未定义的;传递给printf() 的其他两个值i 是否在另一个函数调用的双倍递增之前、之后或期间进行评估也是未定义的。
  • 简单点就是函数参数的求值顺序是unspecified

标签: c++ c printf undefined-behavior sequence-points


【解决方案1】:

我认为您误解了有关 printf 序列点 (SP) 的文本。它们在某种程度上是异常的,并且只有 %n 才有,因为这个格式说明符有副作用,这些副作用需要排序。

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

在您的示例中,i 的使用都在函数参数中,并且它们都没有用序列点分隔。由于您修改了该值(两次)并使用该值而不干预序列点,因此您的代码是 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 表示到目前为止写入的字符数被复制到相关参数指向的整数中。

【讨论】:

  • 大部分是正确的,但是“它们都没有用序列点分隔”并不完全准确。具体来说,在评估your_function()(和“函数指示符”)的参数之后和调用函数之前有一个序列点。因此,在调用该函数之前,两个增量都已完成。你不能说的是 printf() 的其他参数是在调用 your_function() 时评估的——这是未定义的,所以传递给 printf() 的值是未定义的。
  • @JonathanLeffler:由于printf()your_function() 的所有参数都可以在没有中间SP 的情况下进行评估,所以我想说“它们都没有被SP 分开”或多或少是准确的(我应该说“排序”而不是“分离”吗?)。您指向的 SP 实际上对 your_function()printf() 的调用进行排序。
  • 排序。很乱。强调的是,在评估 your_function() 的参数之后和调用函数之前有一个序列点。因此,printf() 的第三个参数在对 your_function() 的调用返回之前无法计算;因此,该调用必须在调用printf() 之前先于SP。但除此之外,printf() 的参数的评估顺序没有限制。在这个阶段,这些细节并不重要——整体行为是 UB,因为 i 的两个增量,任何事情都可能因 UB 而发生。
  • @JonathanLeffler:确实很混乱。这就是为什么 C++ 用“sequenced before/after”取代“sequence point”概念的原因。但我不同意“因此,在对 your_function() 的调用返回之前,无法评估 printf() 的第三个参数”。 f1(a, f2(b, c), d) 的有效评估顺序可以是 dbcf2()af1()。唯一排序的评估是bcf2()f2()adf1() 之前。
  • 您的“我不同意”评论然后继续陈述我认为我所说的内容(注意your_function() 的结果是 printf() 的第三个参数),所以……让我们把它留在“混乱”并结束讨论。我注意到引用的声明实际上来自维基百科,而不是标准。我试图在标准中找到它,但找不到类似的文字。这很好,因为我无法弄清楚“内部序列点”声明的意义是什么——何时可以检测到转换规范之间是否存在序列点。
【解决方案2】:

在评估顺序和 UB 上的 C 规则强烈影响(甚至阻止)得出这个问题的明确答案。

此处说明了有关评估顺序的指定规则:

C99 第 6.7.9 节,p23:23 初始化列表的评估 表达式之间的顺序是不确定的 因此,任何副作用发生的顺序都是未指定的。

还有,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 会发生什么是任何人的猜测。

【讨论】:

  • 我已经编辑了问题以提供澄清:“我不是在问为什么我的中间期限是 0。我是在问为什么其他两个期限都是 12。”
  • @JonathanMee - 由于评估顺序的规则,可以首先评估中间参数,从而导致至少自身(your_function())的 UB,并可能对后续结果产生下游影响.那么UB不可能渗透到整个函数:printf("%d - %d - %d\n", i, your_function(++i, ++i), i); ?
  • 您的说法“实际上任何事情都可能发生”是错误的。函数返回有一个序列点,所以your_function被调用后,i为12。这个问题是关于@上的语句是否987654323@ 意味着我可以依赖按顺序调用的printf 参数。
  • @JonathanMee - 该声明是对它所遵循的链接的嘲讽,其中鼻恶魔用于描述 UB。但是,如果这对您有问题,我会更改它。 然而,我不同意你的说法:所以调用your_function之后,我就12岁了。该功能是UB的原因。无法保证 i 会是什么。 Elias 在他的回答中很好地解释了这一点。 (往下看)
  • your_function 不是 UB 的原因,即使只是 printf("%d%d", i, ++i); 根据 §1.9/15 (N3690) 表现出未定义的行为。 UB 的原因是有两个副作用或一个副作用和一个值计算,它们在一个标量对象上是无序的。 -- 我应该澄清一下,在我的示例中,虽然函数参数的值计算只是不确定的排序,但++i 的副作用与i 的值计算相比是无序的。
【解决方案3】:

因为基于评论的讨论here 提出了这个问题,所以我将提供一些上下文:

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

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

回复该评论:感谢您的评论,我看到printf 可以按任何顺序进行评估,但我理解这是因为printf 参数是@987654326 的一部分@。你是说任何函数的参数都是按任意顺序执行的?

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

第二条评论:是的,这正是我要说的。即使调用 int your_function(int a, int b) { return a - b; } 也不能保证您传递的表达式将从左到右进行计算。没有序列点(执行先前评估的所有副作用的点)。采取this example。嵌套调用是一个序列点,所以外部调用传递i+1 (13),内部调用的返回值(未定义,在本例中为-1,因为i++i 显然计算为12、13 ),但不能保证永远都是这样

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


维基百科混乱

OP 引用这个:

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

然后将其应用于他的 sn-p (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!!

有一个序列点在调用函数之前,所以表达式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

如您所见,nprintfinside 中被重新分配,因此您不能在参数列表中使用它的新值(因为有一个序列点)。如果您希望 n 被“就地”重新分配,那么您实际上是希望 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会增加两次。这个函数调用是一个序列点,因为:

在函数调用中输入函数之前。参数的计算顺序没有指定,但是这个顺序点意味着在进入函数之前它们的所有副作用都已经完成

这意味着,在调用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

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

【讨论】:

    猜你喜欢
    • 2022-11-09
    • 2021-09-07
    • 2013-11-23
    • 1970-01-01
    • 2023-04-08
    • 1970-01-01
    • 2013-06-05
    • 1970-01-01
    • 2022-01-20
    相关资源
    最近更新 更多