10 个自增典型例题解析

作者 Shilei Tian 日期 2017-03-02
C++
10 个自增典型例题解析

第 1 题

源码

1
2
3
int x = 5;
x++;
printf("%d", x);

解析

这道题目没有什么好解释的,在第二行处进行后置自增后,x 变成 6

第 2 题

源码

1
2
3
int x = 5;
++x;
printf("%d", x);

解析

这道题目同样没有什么好解释的,在第二行处进行后置前置后,x 变成 6

第 3 题

源码

1
2
3
int x = 5;
y = x++;
printf("%d,%d", x, y);

解析

在第二行中,由于是后置自增,因此先将 x 赋值给 y 后,完成 x 的自增,因此执行这一句后,y 的值是 5x 的值是 6

第 4 题

源码

1
2
3
int x = 5;
y = ++x;
printf("%d,%d", x, y);

解析

在第二行中,由于是前置自增,因此先将 x 自增后赋值给 y,因此执行这一句后,y 的值是 6x 的值是 6

第 5 题

源码

1
2
3
int x = 5, y;
y = -x++;
printf("%d,%d", x, y);

解析

从这一道题目开始就要用到运算符优先级的知识了。我们这里先讲一下什么是运算符的优先级。复合表达式是指含有两个或多个运算符的表达式。求复合表达式的值需要首先将运算符和运算对象合理地组合在一起,优先级与结合律决定了运算对象组合的方式。也就是说,它们决定了表达式中每个运算符对应的运算对象来自表达式哪一部分。如果优先级相同,则其组合规律由结合律确定。注意我们前面的黑体字部分:优先级只是决定了运算对象的组合方式并不能决定运算对象的求值顺序。在大多数情况下,不会明确指定求值的顺序。

拿我们第 5 题来说,这里一共出现 3 个运算符:=-后置 ++。根据我们的运算符优先级表,我们可以得出,优先级的排序为 后置 ++ > -(一元) > =,那我们整个表达式中对象的组合方式就明确了:y = -(x++)。因此,我们的 y 就应该等于负的 x-5,然后再对 x 进行自增,这样,x 最终是 6

第 6 题

源码

1
2
3
int x = 5, y;
y = x+++x+++x;
printf("%d,%d", x, y);

解析

有了上面的解析,我们这道题目就能够比较容易的将第二行对应的表达式中的对象进行组合。后置自增的运算符优先级要大于加法运算符,因此我们先可以将第一个后置运算符给组合起来,得到 y = (x++) + x+++x。对于剩下的部分,依然可以进行后置运算符组合,得到 y = (x++) + (x++) + x,这样,我们整个表达式就结合完了。

下面来到了这道题目最关键的地方:那对于 (x++)(x++)x 这三个部分,运算顺序是怎么样的?那么我会告诉你:不一定!这个就是要看编译器了。比如说,老师给的答案,这道题目执行后 x 的值是 7y 的值是 15,但是,我的电脑上,y 的值却是 18!还记得我们上面说,优先级只是决定了运算对象的组合方式并不能决定运算对象的求值顺序。 而这道题目就是这句话最典型的例子了。所以我再强调一遍:运算对象的求值顺序与优先级和结合律无关

好了,我们来分析一下,我的电脑上 y18 是怎么来的。x7 这个毫无疑问的。18 = 5 + 6 + 7,也就是说,我的电脑上这条语句的求值顺序的是从左向右。在大家的电脑上用 Code::Block 16.01 应该也是同样的结果吧?

那老师的这个 15 是怎么来的呢?我几乎将所有的编译器都试过了,终于发现了一个输出 15 的编译器,这个编译器就是 Visual Studio 用的编译器。我们为了一探究竟,需要查看这段代码反汇编以后的指令,其中第二行对应的反汇编指令如下:

1
2
3
4
5
6
7
8
9
10
11
12
; int x = 5, y = x++ + x++ + x;
mov dword ptr [x],5
mov eax,dword ptr [x]
add eax,dword ptr [x]
add eax,dword ptr [x]
mov dword ptr [y],eax
mov ecx,dword ptr [x]
add ecx,1
mov dword ptr [x],ecx
mov edx,dword ptr [x]
add edx,1
mov dword ptr [x],edx

注意看第一行:y = x++ + x++ + x。我们输入的代码可没有空格,但是生成的反汇编代码自动给我们加上空格,将这一个表达式断开了,这个断开的结果实际上和我们前面手动加上括号的结果是一样的。

代码的第二行你可以理解成将 5 放到了 [x] 中(当然这么理解实际上不太准确,但不影响我们这里的解释),然后将 [x] 的值放到寄存器 eax 中。下面第三行和第四行很关键,它连续两次将 [x] 的值加到 eax 中,这样经过两次加 5eax 的值就变成了 15,第六行就将 eax 的值赋给 y 了,所以,y = 5 + 5 + 5,这个 15 就是这么来的。剩下的几行就是对 x 进行两次自增,x 就变成 7 了。

第 7 题

源码

1
2
3
int x = 5, y;
y = ++x+(++x)+x++;
printf("%d,%d", x, y);

解析

看到第二行后,我们同样需要拿运算符的优先级来先将对象组合起来。前置自增运算符的优先级依然高于加法运算符,因此第二个表达式等价于 y = (++x) + (++x) + (x++)x 自增了三次,所以最终是 8 这个毫无疑问。

对于 y 的值,我们又出现了不同的运算结果。我的电脑得到的是 20,而 Code::Blocks 16.01Visual Studio 2015 可以得到老师的结果 21。由于这道题我已经有详细的解释,这里我们不再赘述,可以参考这里

第 8 题

源码

1
2
3
int x = 5, y;
y = ++x+(++x)+(++x);
printf("%d,%d", x, y);

解析

看到第二行后,我们同样需要拿运算符的优先级来先将对象组合起来。前置自增运算符的优先级依然高于加法运算符,因此第二个表达式等价于 y = (++x) + (++x) + (++x)x 自增了三次,所以最终是 8 这个毫无疑问。

对于 y 的值,我们又出现了不同的运算结果。我的电脑得到的是 21,而 Code::Blocks 16.01 得到的是 22Visual Studio 2015 得到的结果竟然是 24

我们这里只讲老师的答案 22 是怎么得到的。继续看反汇编代码:

1
2
3
4
5
6
7
8
9
10
; int x = 5, y=++x+(++x)+(++x);
movl $0x5,0x1c(%esp)
addl $0x1,0x1c(%esp)
addl $0x1,0x1c(%esp)
mov 0x1c(%esp),%eax
lea (%eax,%eax,1),%edx
addl $0x1,0x1c(%esp)
mov 0x1c(%esp),%eax
add %edx,%eax
mov %eax,0x18(%esp)

在第二行将 5 放到寄存器 esp 中,然后连续两次对 esp 进行加 1,这样 esp 里面的值就是 7。然后将 esp 的值挪到 eax 中。第六行的指令很关键,相当于 %edx = %eax + %eax * 1,这样 edx 里面就是 14 了。 下面继续对 esp 进行加一,这样我们的 esp 就等于 8 了。在第八行中将 esp 的挪到 eax 中,然后将 eax 的值加到 edx 上,这时 edx 的值变成 22。然后又将 esp 的值挪到 eax 中。最终,edx 中存放的是 y 的值,eax 中存放的是 x 的值。

所以,这个 22 = 7 + 7 + 8 得到。

第 9 题

源码

1
2
int x = 5;
printf("%d, %d", ++x, x);

解析

这道题目难度就下来了,第一个 %d 对应的是 x 先进行自增得到的值,为 6,此时 x 也变成 6

第 10 题

源码

1
2
int x = 5;
printf("%d", x+++x+++x);

解析

这道题目和第 6 题其实是一样的,不再赘述。

总结

对于这一种在 C++ 标准里面未明确定义的语句,它的执行就看不同编译器的实现了。所以我们在自己写代码时,一定要避免这种未定义的行为。