为什么这些构造使用前后递增的未定义行为?

人走茶凉 提交于 2019-12-19 15:48:41

【推荐】2019 Java 开发者跳槽指南.pdf(吐血整理) >>>

#include <stdio.h>

int main(void)
{
   int i = 0;
   i = i++ + ++i;
   printf("%d\n", i); // 3

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

   volatile int u = 0;
   u = u++ + ++u;
   printf("%d\n", u); // 1

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

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

   int w = 0;
   printf("%d %d\n", ++w, w); // shouldn't this print 1 1

   int x[2] = { 5, 8 }, y = 0;
   x[y] = y ++;
   printf("%d %d\n", x[0], x[1]); // shouldn't this print 0 8? or 5 0?
}

#1楼

尽管不太可能有任何编译器和处理器实际执行此操作,但是在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

虽然我不认为任何处理器都支持硬件来有效地完成这样的事情,但人们可以轻松想象这种行为会使多线程代码更容易的情况(例如,如果两个线程尝试执行上述操作,则可以保证这种情况)序列同时, i将增加两个),并且将来的处理器可能会提供类似的功能并不是完全不可想象的。

如果编译器按照上述指示编写i++ (根据标准合法)并在整个表达式求值过程中散布以上指令(也是合法的),并且没有注意到其他指令之一碰巧访问了i ,编译器可能会(并且合法)生成一系列死锁的指令。 可以肯定的是,在两个地方都使用相同变量i的情况下,但是如果例程接受对两个指针pq引用,并使用(*p)(*q) ,则编译器几乎可以检测到问题。在上面的表达式中(而不是使用i两次),不需要编译器识别或避免如果为pq传递了相同对象的地址时将发生死锁。


#2楼

该行为无法真正解释,因为它同时调用了未指定的行为未定义的行为 ,因此我们无法对此代码做出任何一般性的预测,尽管如果您阅读Olve Maudal的著作(例如Deep CUnspecified and Undefined),有时可以在特定情况下使用特定的编译器和环境进行猜测,但是请不要在生产环境附近这样做。

因此,继续进行未指定的行为 ,在c99标准草案6.5节第3段中说( 强调我的 ):

语法表示操作符和操作数的分组。74)除非稍后指定(对于函数调用(),&&,||,?:和逗号运算符), 子表达式的求值顺序和哪种副作用都未指定。

因此,当我们有这样的一行时:

i = i++ + ++i;

我们不知道会先评估i++还是++i 。 这主要是为编译器提供更好的优化选项

由于程序在序列点之间多次修改变量( iu等),因此我们在这里也具有未定义的行为 。 从标准草案第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节中定义为:

在使用非便携式或错误程序构造或错误数据时的行为,对此国际标准不施加任何要求

并指出:

可能的不确定行为范围从完全忽略具有无法预测结果的情况到在翻译或程序执行过程中以环境特征记录的方式表现(带有或不带有诊断消息)到终止翻译或执行(带有发布)诊断消息)。


#3楼

C标准说,一个变量最多只能在两个序列点之间分配一次。 例如,分号是一个序列点。
因此,每个形式的语句:

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

以此类推。 该标准还规定行为是不确定的,不是不确定的。 一些编译器确实会检测到这些并产生一些结果,但这不是每个标准的结果。

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

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

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


#4楼

如果您很想知道代码的确切程度,那么只需编译和反汇编您的代码即可。

这是我在机器上得到的,以及我认为正在发生的事情:

$ 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.

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


#5楼

https://stackoverflow.com/questions/29505280/incrementing-array-index-in-c中,有人问到以下语句:

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 ,然后读取k[]

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

现代编译器将对此进行很好的优化。 实际上,它可能比您最初编写的代码更好(假设它按照您希望的方式工作)。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!