#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?
}
当前回答
只要编译和反汇编你的代码行,如果你如此倾向于知道它是如何得到你所得到的。
这是我从我的机器上得到的,以及我认为正在发生的事情:
$ 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指令是某种编译器优化?)
其他回答
C有未定义行为的概念,也就是说,一些语言结构在语法上是有效的,但你不能预测代码运行时的行为。
据我所知,该标准并没有明确说明为什么存在未定义行为的概念。在我看来,这只是因为语言设计者想在语义上有一些余地,而不是要求所有实现以完全相同的方式处理整数溢出,这很可能会带来严重的性能损失,他们只是让行为未定义,所以如果你编写的代码导致整数溢出,任何事情都可能发生。
那么,考虑到这一点,为什么这些是“问题”呢?语言清楚地表明,某些事情会导致未定义的行为。没有什么问题,没有什么“应该”牵涉其中。如果当其中一个涉及的变量被声明为volatile时,未定义的行为发生了变化,这并不能证明或改变任何东西。它是没有定义的;你不能对这种行为进行推理。
你最有趣的例子就是
u = (u++);
是教科书上未定义行为的例子(见维基百科关于序列点的条目)。
这个问题通常是与代码相关的问题的重复链接,比如
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++ 98标准不需要序列点(根据c++ 11术语,没有任何操作在另一个操作之前或之后排序)。
然而,如果你坚持使用一个编译器,你会发现这种行为是持久的,只要你不添加函数调用或指针,这将使行为更加混乱。
使用Nuwen MinGW 15 GCC 7.1你会得到:
#include<stdio.h>
int main(int argc, char ** argv)
{
int i = 0;
i = i++ + ++i;
printf("%d\n", i); // 2
i = 1;
i = (i++);
printf("%d\n", i); //1
volatile int u = 0;
u = u++ + ++u;
printf("%d\n", u); // 2
u = 1;
u = (u++);
printf("%d\n", u); //1
register int v = 0;
v = v++ + ++v;
printf("%d\n", v); //2
}
How does GCC work? it evaluates sub expressions at a left to right order for the right hand side (RHS) , then assigns the value to the left hand side (LHS) . This is exactly how Java and C# behave and define their standards. (Yes, the equivalent software in Java and C# has defined behaviors). It evaluate each sub expression one by one in the RHS Statement in a left to right order; for each sub expression: the ++c (pre-increment) is evaluated first then the value c is used for the operation, then the post increment c++).
根据GCC c++:操作符
在GCC c++中,操作符的优先级控制在 哪些操作符被求值
GCC所理解的定义行为c++中的等效代码:
#include<stdio.h>
int main(int argc, char ** argv)
{
int i = 0;
//i = i++ + ++i;
int r;
r=i;
i++;
++i;
r+=i;
i=r;
printf("%d\n", i); // 2
i = 1;
//i = (i++);
r=i;
i++;
i=r;
printf("%d\n", i); // 1
volatile int u = 0;
//u = u++ + ++u;
r=u;
u++;
++u;
r+=u;
u=r;
printf("%d\n", u); // 2
u = 1;
//u = (u++);
r=u;
u++;
u=r;
printf("%d\n", u); // 1
register int v = 0;
//v = v++ + ++v;
r=v;
v++;
++v;
r+=v;
v=r;
printf("%d\n", v); //2
}
然后我们去Visual Studio。Visual Studio 2015,你得到:
#include<stdio.h>
int main(int argc, char ** argv)
{
int i = 0;
i = i++ + ++i;
printf("%d\n", i); // 3
i = 1;
i = (i++);
printf("%d\n", i); // 2
volatile int u = 0;
u = u++ + ++u;
printf("%d\n", u); // 3
u = 1;
u = (u++);
printf("%d\n", u); // 2
register int v = 0;
v = v++ + ++v;
printf("%d\n", v); // 3
}
Visual Studio是如何工作的,它采用了另一种方法,它在第一遍计算所有的前增量表达式,然后在第二遍操作中使用变量值,在第三遍将RHS赋值给LHS,然后在最后一遍计算所有的后增量表达式。
因此,在定义行为的c++中,就像Visual c++理解的那样:
#include<stdio.h>
int main(int argc, char ** argv)
{
int r;
int i = 0;
//i = i++ + ++i;
++i;
r = i + i;
i = r;
i++;
printf("%d\n", i); // 3
i = 1;
//i = (i++);
r = i;
i = r;
i++;
printf("%d\n", i); // 2
volatile int u = 0;
//u = u++ + ++u;
++u;
r = u + u;
u = r;
u++;
printf("%d\n", u); // 3
u = 1;
//u = (u++);
r = u;
u = r;
u++;
printf("%d\n", u); // 2
register int v = 0;
//v = v++ + ++v;
++v;
r = v + v;
v = r;
v++;
printf("%d\n", v); // 3
}
如Visual Studio文档在优先级和计算顺序中所述:
当几个运算符同时出现时,它们具有相同的优先级,并根据它们的结合性进行计算。表中的操作符在以后缀操作符开头的部分中描述。
这里的大多数答案都引用自C标准,强调这些构造的行为是未定义的。为了理解为什么这些结构的行为是未定义的,让我们先从C11标准的角度来理解这些术语:
测序(5.1.2.3):
给定任意两个评估A和B,如果A排在B之前,则A的执行应先于B的执行。
Unsequenced:
如果A不在B之前或之后测序,则A和B是未测序的。
评估可能是以下两种情况之一:
值计算,计算出表达式的结果;而且 副作用,也就是对对象的修改。
序列:
在表达式a和表达式B的求值之间存在序列点,意味着与a相关的每个值计算和副作用都在与B相关的每个值计算和副作用之前排序。
现在回到问题,对于像这样的表达
int i = 1;
i = i++;
标准说:
6.5表达式:
如果标量对象上的副作用相对于同一标量对象上的不同副作用或使用同一标量对象的值进行的值计算没有排序,则行为未定义。[…]
因此,上面的表达式调用UB,因为同一对象i上的两个副作用彼此之间没有顺序。这意味着赋值给i的副作用是在++的副作用之前还是之后,没有排序。 根据赋值是发生在增量之前还是之后,将产生不同的结果,这就是未定义行为的情况之一。
将赋值左边的i重命名为il,赋值右边(表达式i++)重命名为ir,则表达式为
il = ir++ // Note that suffix l and r are used for the sake of clarity.
// Both il and ir represents the same object.
关于postfix++操作符的重要一点是:
仅仅因为++出现在变量之后并不意味着增量发生得晚。只要编译器确保使用原始值,增量可以在编译器喜欢的时间发生。
这意味着表达式il = ir++可以被求值为
temp = ir; // i = 1
ir = ir + 1; // i = 2 side effect by ++ before assignment
il = temp; // i = 1 result is 1
or
temp = ir; // i = 1
il = temp; // i = 1 side effect by assignment before ++
ir = ir + 1; // i = 2 result is 2
导致两个不同的结果1和2,这取决于通过赋值和++的副作用的顺序,因此调用UB。