#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?
}
当前回答
C标准规定,一个变量最多只能在两个序列点之间赋值一次。例如,分号是一个序列点。 所以每个形式的表述
i = i++;
i = i++ + ++i;
等等都违反了这个规则。该标准还说行为是未定义的,而不是未指定的。一些编译器确实检测这些并产生一些结果,但这不是每个标准。
但是,两个不同的变量可以在两个序列点之间递增。
while(*src++ = *dst++);
以上是在复制/分析字符串时常见的编码实践。
其他回答
这个问题通常是与代码相关的问题的重复链接,比如
printf("%d %d\n", i, i++);
or
printf("%d %d\n", ++i, i++);
或者类似的变体。
虽然正如前面所述,这也是未定义的行为,但当涉及printf()时,与如下语句进行比较时,会有细微的差异:
x = i++ + i++;
在以下声明中:
printf("%d %d\n", ++i, i++);
printf()中参数的求值顺序未指定。这意味着,表达式i++和++i可以以任何顺序求值。C11标准对此有一些相关的描述:
附件J,未指明的行为
函数指示符、参数和 参数中的子表达式在函数调用中求值 (6.5.2.2)。
3.4.4,未指定的行为
使用未指定的值,或其他行为 国际标准提供了两种或两种以上的可能性 在任何情况下都没有进一步的要求。 未指定的行为的一个例子是 函数的参数被求值。
未指定的行为本身不是问题。想想这个例子:
printf("%d %d\n", ++x, y++);
这也具有未指定的行为,因为++x和y++的求值顺序是未指定的。但这是完全合法有效的声明。在这个语句中没有未定义的行为。因为修改(++x和y++)是对不同的对象进行的。
是什么呈现下面的语句
printf("%d %d\n", ++i, i++);
作为未定义的行为是这两个表达式修改相同的对象I没有中间序列点的事实。
另一个细节是printf()调用中涉及的逗号是分隔符,而不是逗号操作符。
这是一个重要的区别,因为逗号操作符确实在它们的操作数求值之间引入了一个序列点,这使得以下行为合法:
int i = 5;
int j;
j = (++i, i++); // No undefined behaviour here because the comma operator
// introduces a sequence point between '++i' and 'i++'
printf("i=%d j=%d\n",i, j); // prints: i=7 j=6
逗号操作符从左到右计算其操作数,只产生最后一个操作数的值。所以在j = (++i, i++);中,++i将i增加到6,i++产生i(6)的旧值,该值被分配给j。然后由于后增量,i变成7。
如果函数调用中的逗号是逗号操作符,那么
printf("%d %d\n", ++i, i++);
不会有问题的。但是它调用了未定义的行为,因为这里的逗号是分隔符。
对于那些不熟悉未定义行为的人来说,阅读《每一个C程序员都应该知道关于未定义行为的事情》可以帮助他们理解C语言中未定义行为的概念和许多其他变体。
本文:未定义、未指定和实现定义的行为也是相关的。
这种行为实际上无法解释,因为它同时调用了未指定的行为和未定义的行为,所以我们不能对这段代码做出任何一般的预测,尽管如果你阅读了Olve Maudal的著作,比如《Deep C》和《未指定的和未定义的》,有时你可以用特定的编译器和环境在非常特定的情况下做出很好的猜测,但请不要在生产环境附近这样做。
所以我们继续讨论未指明的行为,在c99标准草案第6.5节第3段说(我的重点):
操作符和操作数的分组由语法表示。除非指定 稍后(对于函数call()、&&、||、?:和逗号操作符),子表达式的求值顺序和副作用发生的顺序都未指定。
所以当我们有一条这样的直线
i = i++ + ++i;
我们不知道是i++还是++i先被求值。这主要是为了给编译器提供更好的优化选项。
这里也有未定义的行为,因为程序在序列点之间不止一次地修改变量(i, u等)。标准草案第6.5节第2段(重点):
在前一个序列点和下一个序列点之间,一个对象应该有它的存储值 由表达式求值最多修改一次。此外,先验值 应该是只读的,以确定要存储的值。
它引用了以下未定义的代码示例:
i = ++i + 1;
a[i++] = i;
在所有这些示例中,代码都试图在同一序列point中多次修改一个对象,该序列point将以;在以下每一种情况下:
i = i++ + ++i;
^ ^ ^
i = (i++);
^ ^
u = u++ + ++u;
^ ^ ^
u = (u++);
^ ^
v = v++ + ++v;
^ ^ ^
在c99标准草案第3.4.4节中,未指定的行为定义为:
使用未指定的值,或本标准规定的其他行为 两种或两种以上的可能性,并且对选择哪一种没有进一步的要求 实例
而未定义的行为在3.4.3节中定义为:
在使用不可移植的或错误的程序构造或错误的数据时, 本标准对此没有任何要求
并指出:
可能的未定义行为包括:完全忽略带有不可预测结果的情况,在翻译或程序执行期间以环境特征的文档化方式(有或没有发出诊断消息)行为,以及终止翻译或执行(发出诊断消息)。
C有未定义行为的概念,也就是说,一些语言结构在语法上是有效的,但你不能预测代码运行时的行为。
据我所知,该标准并没有明确说明为什么存在未定义行为的概念。在我看来,这只是因为语言设计者想在语义上有一些余地,而不是要求所有实现以完全相同的方式处理整数溢出,这很可能会带来严重的性能损失,他们只是让行为未定义,所以如果你编写的代码导致整数溢出,任何事情都可能发生。
那么,考虑到这一点,为什么这些是“问题”呢?语言清楚地表明,某些事情会导致未定义的行为。没有什么问题,没有什么“应该”牵涉其中。如果当其中一个涉及的变量被声明为volatile时,未定义的行为发生了变化,这并不能证明或改变任何东西。它是没有定义的;你不能对这种行为进行推理。
你最有趣的例子就是
u = (u++);
是教科书上未定义行为的例子(见维基百科关于序列点的条目)。
我认为C99标准的相关部分是6.5表达式,§2
在前一个序列点和下一个序列点之间,一个对象应该有它的存储值 由表达式求值最多修改一次。此外,先验值 应该是只读的,以确定要存储的值。
和6.5.16赋值操作符,§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指令是某种编译器优化?)