【问题标题】:Is foo(i++) + foo(i++) undefined in ANSI C?ANSI C 中未定义 foo(i++) + foo(i++) 吗?
【发布时间】:2014-01-28 16:02:36
【问题描述】:

这是一个示例 sn-p:

int i = 4,b;    
b = foo(i++) + foo(i++);

我很确定它不是未定义,因为在调用foo 之前有一个序列点。但是,如果我使用-Wall 标志编译代码,则会生成一个编译器警告,上面写着warning: operation on 'i' may be undefined。我知道上面写着may,但我想仔细检查一下我是否正确。

【问题讨论】:

  • 不管有没有定义,都不应该这样编程。
  • 在调用foo 之前有一个序列点,但在i++ 的两次评估之间没有必然的序列点。生成的代码可以评估第一个i++,然后评估第二个i++,然后执行两个函数调用,然后将结果相加。 C11(参见N1570 draft 改变了描述的方式,可能更清楚。
  • @JonasWielicki:不,无论如何它都是未定义的,因为 i 被修改了两次,没有插入序列点(在 C99 术语中)。
  • @mafso:正如我上面所说,我相信i++ 的两个实例都可以在任何一个函数调用发生之前进行评估。 N1570 6.5.2.2p10:“在函数指示符和实际参数的评估之后但在实际调用之前有一个序列点。调用函数(包括其他函数调用)中的每个评估之前没有特别排序或者在被调用函数体的执行相对于被调用函数的执行顺序不确定之后。”

标签: c undefined-behavior sequence-points


【解决方案1】:

行为未定义。

b = foo(i++) + foo(i++);

正如你所说,在第一个i++ 的评估和对foo 的调用之间存在一个序列点,在第二个i++ 的评估和对foo 的调用之间也是如此。但是i++ 的两次评估之间没有(必然)一个序列点,或者更具体地说,在它们的副作用之间(修改i)。

引用 2011 ISO C 标准的N1570 草案,第 6.5.2.2p10 节:

函数求值后有一个序列点 指示符和实际参数,但在实际调用之前。每一个 调用函数中的求值(包括其他函数调用) 之前或之后没有特别排序的 被调用函数体的执行是不确定的 根据被调用函数的执行顺序。

这里第二句意义重大:i++ 的两个评价是“不确定 对这两个函数调用而言,它们是有序的”,这意味着它们可以在调用 foo 之前或之后发生。(不过,它们不是未排序的;它们中的每一个都发生在之前或之后调用,但未指定哪个。)

而 6.5p2 说:

如果标量对象的副作用相对于其中任一对象而言是未排序的 对同一标量对象或值的不同副作用 使用相同标量对象的值进行计算,行为是 不明确的。如果有多个允许的排序 表达式的子表达式,如果这样的表达式的行为是未定义的 任何排序都会出现未排序的副作用。

将这些放在一起,符合要求的实现可以按以下顺序评估表达式:

  1. 评估第一个 i++ 并将值保存在某处。
  2. 计算第二个i++并将值保存在某处。
  3. 调用foo,将第一个保存的值作为参数传递。
  4. 调用foo,将第二个保存的值作为参数传递。
  5. 将两个结果相加。
  6. 将总和存储在b

步骤1和2之间没有序列点,都修改i,所以行为未定义。

(这其实是有点过于简单化了;修改i的副作用可以和i++的结果判断分开。

底线:我们知道

b = i++ + i++;

具有未定义的行为,原因已反复解释。在函数调用中包装 i++ 子表达式确实会添加一些序列点,但这些序列点不会将 i++ 的两个评估分开,因此不会导致行为变得明确。

即使是底线:请不要编写那样的代码。即使行为被很好地定义,证明它并确定行为应该是什么也将比它的价值更困难。

【讨论】:

  • 我已经阅读了您复制粘贴的标准部分 3-4 遍,但仍然没有真正理解它,尽管我觉得您的解释是正确的。无论哪种方式我都不会这样编程,我只是发布这个问题来更好地理解序列点。
  • 如果你需要理解序列点,你的编码很糟糕。
  • @MartinJames 这是一个纯粹的学术问题。只是想更好地理解 C。
猜你喜欢
  • 2015-09-12
  • 2022-11-30
  • 1970-01-01
  • 1970-01-01
  • 2014-05-06
  • 2017-02-15
  • 2011-04-09
  • 1970-01-01
相关资源
最近更新 更多