increment - 为什么这些结构( 使用 ) 未定义的行为?

  显示原文与译文双语对照的内容

int main(int argc, char ** argv)
{
 int i = 0;
 i = i++ + ++i;
 printf("%dn", i);//3

 i = 1;
 i = (i++);
 printf("%dn", i);//2 Should be 1, no?

 volatile int u = 0;
 u = u++ + ++u;
 printf("%dn", u);//1

 u = 1;
 u = (u++);
 printf("%dn", u);//2 Should also be one, no?

 register int v = 0;
 v = v++ + ++v;
 printf("%dn", v);//3 (Should be the same as u?)
}

时间:

为什么这些"问题"语言清楚地说,某些东西导致了未定义的行为 。 没有问题,没有"应该应该"参与。 如果未定义行为变化时涉及的变量之一是宣布 volatile, 这并不证明或者改变什么。 它是的,你不能理性的行为。

你最常用的interesting-loooking示例,即

 
u = (u++);

 

是未定义行为的text-book示例( 关于序列点的维基百科条目参见参考资料) 。

从FAQ中读取这个问题。

Q: 如何理解这一节中的复杂表达式,避免编写未定义的表达式? 什么是"序列点"?

一个序列点是一个时间点,在那里尘埃已经解决了,所有的副作用都被认为是完整的。 在C 标准中列出的序列点是:

  1. 对完整表达式( 完整表达式是表达式语句,或者任何其他表达式中不是子表达式的表达式) 求值结束时;
  2. ||&&?: 和逗号运算符处;以及
  3. 在 function call (after the evaluation of all the arguments, and just before the actual call) 处。

标准状态是

在上一个序列和下一个序列点之间,一个对象应该通过对表达式的求值一次修改它的存储值。 此外,应该只访问以前的值以确定要存储的值。

这两个相当不透明的句子说了几个东西。 首先,他们讨论由"上一个和下一个序列点"界定的操作;这些操作通常对应于完整表达式。 ( 在表达式语句中,"下一个序列点"通常位于终止分号,而"上一个序列点"位于前一条语句的末尾) 。 表达式也可以包含中间序列点,如上所述。

第一个句子排除了两个例子

 
i++ * i++

 

 
i = i++

 

从问题 3.2和 3.3 --in中,我在表达式的表达式中修改了两次,在序列点之间修改了 换句话说,。 ( 如果我们写一个类似的表达式,它有一个内部序列点,比如

 
i++ && i++

 

如果questionably有用的话,它就会被定义。

第二个句子很难理解。 事实证明,它不允许代码

 
a[i] = i++

 

从问题 3.1.( 实际上,我们讨论的其他表达式违反了第二个句子。) 了解为什么,让我们先看一下标准试图允许和不允许的标准。

显然,这样的表达式

 
a = b

 


c = d + e

它读取一些值并使用它们来编写其他的值,它们是定义良好和合法的。 显然,[footnote] 表达式

 
i = i++

 

两次修改相同值的值是 abominations,不需要( 或者,在任何情况下都不需要定义,换句话说,不需要找出他们做什么的方法,编译器不必支持它们) 。 第一个句子不允许这样的表达式。

[footnote] 也很清楚,我们想不允许这样的表达式

 
a[i] = i++

 

我并不不允许这样的表达式,但不允许


i = i + 1

使用和修改我只有修改后,它是相当容易确保最后的最后存储值( 在 i 中,在本例中) 不会干扰早些时候访问。

第二个句子就是这样说的: 如果一个对象被写到一个完整表达式中,那么在同一个表达式中对它的所有访问都必须直接参与到要写入的值的计算中。 这里规则有效地约束了在修改前明显访问的合法表达式。 例如旧的备用 i = i + 1 允许,因为 i的访问被用来决定值的i 最终值。 例子

 
a[i] = i++

 

无效,因为我的一个访问( a[i] 中的那个) 无关的价值最终被存储在 i ( 在 i++ 中发生), 所以没有好办法define--either为我们理解或编译器的s--whether访问之前或之后应增加价值。 由于没有好的方法来定义它,标准声明它是未定义的,而且便携程序不能使用这种构造。

只需编译并反汇编代码行,如果你这么想知道它到底是如何得到的。

这就是我在我的机器上看到的,以及我想的事情:


$ cat evil.c
void evil(){
 int i = 0;
 i+= i++ + ++i;
}
$ gcc evil.c -c -o evil.bin
$ gdb evil.bin
(gdb) disassemble evil
Dump of assembler code for function evil:
 0x00000000 <+0>: push %ebp
 0x00000001 <+1>: mov %esp,%ebp
 0x00000003 <+3>: sub $0x10,%esp
 0x00000006 <+6>: movl $0x0,-0x4(%ebp)//i = 0 i = 0
 0x0000000d <+13>: addl $0x1,-0x4(%ebp)//i++ i = 1
 0x00000011 <+17>: mov -0x4(%ebp),%eax//j = i i = 1 j = 1
 0x00000014 <+20>: add %eax,%eax//j += j i = 1 j = 2
 0x00000016 <+22>: add %eax,-0x4(%ebp)//i += j i = 3
 0x00000019 <+25>: addl $0x1,-0x4(%ebp)//i++ i = 4
 0x0000001d <+29>: leave 
 0x0000001e <+30>: ret
End of assembler dump.

( 我假设 0 x00000014指令是某种编译器优化)?

我认为C99标准相关的部分是 6.5表达式,§2

在上一个序列和下一个序列点之间,对象应该通过对表达式的求值一次修改它的存储的值一次。 此外,应读取以前的值以确定要存储的值。

和 6.5.16赋值操作符,§4:

操作数的求值顺序未指定。 如果试图修改赋值运算符的结果或者在下一个序列点之后访问它,则该行为未定义。

行为被解释不咋地,因为它将同时调用 和 未指定的行为未定义的行为因此我们不能出现任何常规的预测,关于这段代码虽然如果你读 Olve maudal工作如深C未指定的和未定义的 有时可以能够很好地推断在非常特殊的情况下使用特定的编译器和环境的任何位置,但请不要这样临时生产。

所以继续未指明的行为,在c99标准草案6.5 节段 3 says( emphasis mine ):

运算符和操作数的分组由语法指示。74 ) 除后面指定的( 对于 function-call ( ),&& ;,||? :和逗号操作符),评估子表达式的顺序和副作用发生的顺序都是未指定的。

所以当我们有这样一行:


i = i++ + ++i;

我们不知道是否首先评估 i++ 或者 ++i 。 这主要是为了给编译器更好的优化选项。

我们也有未定义的行为在这里因为程序修改 variables( i, u, etc..) 不止一次 序列点之间。 从草稿标准段 6.5 段落 2 ( 强调矿山 ):

之间的前一个和后一个序列点对象有它最多值修改存储一次评价的一个表达式。 此外,前值应当只读存储确定价值。

它引用了以下代码示例未定义:


i = ++i + 1;
a[i++] = i; 

在所有这些示例中,代码试图在同一个序列点多次修改一个对象,这将以每一种情况下的; 结束:


i = i++ + ++i;
^ ^ ^

i = (i++);
^ ^

u = u++ + ++u;
^ ^ ^

u = (u++);
^ ^

v = v++ + ++v;
^ ^ ^

不明c99标准草案中定义的行为3.4.4 节:

使用未指定的值或者其他的行为,这里国际标准提供两个或者更多的可能性,并不进一步要求在任何实例中选择

未定义的行为在 3.4.3 节中定义为:

使用非可移植或者错误的程序构造或者错误数据时的行为,这里国际标准不要求任何需求

并注意到:

可能未定义的行为范围从忽视情况完全不可预知的结果,在翻译行为或程序执行记录方式的特征环境( 带有或者不发布诊断消息) ( 随着诊断信息的发布) 终止翻译或执行。

虽然不太可能有编译器和处理器实际这么做,但是在C 标准下,编译器使用序列实现"i++"是合法的:


In a single operation, read `i` and lock it to prevent access until further notice
Compute (1+read_value)
In a single operation, unlock `i` and store the computed value

虽然我认为任何处理器都不支持硬件以允许这样做,但可以很容易地想象这样的行为会使multi-threaded代码更加容易( 例如。 这将保证如果两个线程试图同时执行上述序列, i 会增加了两个)这并不是完全不可想象的一些未来的处理器可能提供这样的一个特征。

如果编译器写上述 i++ ( 标准下的法律) 到处散布上述指令的评估整个表达式( 合法也合法), 如果它没有发生注意到的另一个指令访问 i 发生,这将是可能的( 和合法的) 编译器生成一个僵局的指令序列。 于同时 i 和相关肯定地检测该错误,编译器会将几乎肯定在这种情况( 同一个变量 iij 这两个位置都可以使用,但如果是一次例行的接受对两个引用变量,并使用 i 和( 而不是使用 i 两次) j 在上面的表达式所需的编译器就不会识别或者避免而发生死锁。如果相同的变量当作 passed.

标准表示一个变量在两个序列点之间只能被分配一次。 例如semi-colon是一个序列点。
所以形式的每条语句:


i = i++;
i = i++ + ++i;

等等违反了那个规则。 标准还说行为是未定义的并且未指定。 有些编译器检测到这些结果并产生一些结果,但这不是每一个标准。

但是,两个不同的变量可以在两个序列点之间增加。


while(*src++ = *dst++);

上是在复制/分析字符串时常见的编码实践。

请阅读 & R 。 它明确表明这种行为是未定义的。 一些编译器像gcc遵循一些约定但是它的编译器依赖。 所以最好避免在单个命令中更改和使用变量的值,因为相同的内存位置不能同时读取或者写入

中增加数组索引时,有人询问了这样一个语句:


int k[] = {0,1,2,3,4,5,6,7,8,9,10};
int i = 0;
int num;
num = k[++i+k[++i]] + k[++i];
printf("%d", num);

打印 7.。OP预期打印 6.

++i 增量在余下的计算之前不能保证全部完成。 实际上,不同的编译器会得到不同的结果。 在你提供的示例中,执行了前 2个 ++i,然后读取了 k[]的值,然后是最后一个 ++i


num = k[i+1]+k[i+2] + k[i+3];
i += 3

现代编译器将优化这个非常好的。 事实上,可能比你最初编写的( 假设它已经按照你希望的方式工作了) 更好。

...