为何这些构造使用先后递增的未定义行为?

#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 ++”是合法的: c++

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将增长两个),而且未来的处理器可能会提供相似的功能并非彻底不可想象的。 sass

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


#2楼

该行为没法真正解释,由于它同时调用了未指定的行为未定义的行为 ,所以咱们没法对此代码作出任何通常性的预测,尽管若是您阅读Olve Maudal的著做(例如Deep CUnspecified and Undefined),有时能够在特定状况下使用特定的编译器和环境进行猜想,可是请不要在生产环境附近这样作。 函数

所以,继续进行未指定的行为 ,在c99标准草案6.5节第3段中说( 强调个人 ): 优化

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

所以,当咱们有这样的一行时: 编码

i = i++ + ++i;

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

因为程序在序列点之间屡次修改变量( 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

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

相关文章
相关标签/搜索