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