协慌网

登录 贡献 社区

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

#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 %d\n", w++, ++w, w); // shouldn't this print 0 2 2

   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?
}

答案

C 具有未定义行为的概念,即某些语言结构在语法上有效,但您无法预测代码运行时的行为。

据我所知,该标准没有明确说明为什么存在未定义行为的概念。在我看来,这仅仅是因为语言设计者希望在语义上有一些余地,而不是要求所有实现以完全相同的方式处理整数溢出,这很可能会带来严重的性能成本,他们只是留下了行为未定义,以便如果您编写导致整数溢出的代码,任何事情都可能发生。

那么,考虑到这一点,为什么这些 “问题”?该语言清楚地表明,某些事情会导致不确定的行为 。没有问题,没有 “应该” 参与。如果未声明的行为在其中一个涉及的变量被声明为volatile发生更改,则不会证明或更改任何内容。它未定义 ; 你无法推理这种行为。

你最有趣的例子,有一个

u = (u++);

是未定义行为的教科书示例(请参阅维基百科关于序列点的条目)。

只需编译和反汇编您的代码行,如果您倾向于知道它是如何得到您正在获得的。

这就是我在我的机器上得到的,以及我的想法:

$ 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 指令是某种编译器优化?)

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

在前一个和下一个序列点之间,对象的存储值最多只能通过表达式的计算来修改一次。此外,先前的值应该只读以确定要存储的值。

和 6.5.16 分配运算符,§4:

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