我们来看一下C语言中的前自增(++i)和后自增(i++) 这个经典案例。大家在学习C的时候肯定学过前自增是先自增,然后将结果用于计算;后自增是先参与计算,再增加。
好,看一下这段代码的结果:
#include <stdio.h>
int main()
{
int i = 3;
int sum;
sum = i++ + ++i ;
printf("sum is %d.\n",sum);
return 0;
}
- i ++, 相当于先从内存中把这个值取到寄存器,真正参与 "+" 这个运算的是直接从寄存器中取值参与运算。
- ++ i,首先修改内存,参与运算的时候再把它从内存当中取到寄存器。
- 然后两个寄存器参与 "+" 运算(sum = i++ + ++i)
这个题目估计大家问题不大。那,下面的代码呢(手动滑稽hh~)?
#include <stdio.h>
int main()
{
int i = 3;
int sum;
sum = ++i + ++i ;
printf("sum is %d.\n",sum);
return 0;
}
这个输出结果笔者曾一度坚定的认为是9(就算天王老子来了,它也得是9!!!)。
啪啪打脸hh~,我不理解,为啥是10呢?我们下面就来好好分析一下
我们将这两个程序生成的可执行文件反汇编,对比看看有什么不同
下图为第一个程序的汇编代码 (sum = i++ + ++i)
对上述汇编代码做详细解释:
movl $0x3,-0x8(%rbp) 将3移动到-0x8(%rbp)这个内存位置,相当于对变量i进行赋值(i = 3)
接下来要做的是i ++,
mov -0x8(%rbp),%eax 首先把这个内存位置的值拷贝到eax寄存器中(%eax = 3)
lea 0x1(%rax),%edx 然后开始对寄存器eax中的值+1 赋到edx中 (%edx = 4)
mov %edx,-0x8(%rbp) 接下来将edx中的值移动到内存中(i = 4)
接下来要做的是++ i,
addl $0x1,-0x8(%rbp) 直接对内存位置的值+1(i = 5)
到此,i ++ 和 ++ i都已经计算结束,要开始进行加法运算了
左边的值已经保存在eax中了,
mov -0x8(%rbp),%edx 现在要从内存中把i的值读出来放到另一个寄存器edx中(%edx = 5)
执行加法运算时,
add %edx,%eax (%eax += %edx)此时eax的值就是8了
这个程序与我们分析的是一致的,那两个++ i又是什么情况呢?我们来分析一下
(注:为了省事,我直接在图中标注了hh~)
看出来区别了吗?做加法首先要把操作数准备好。
对于i ++,首先做的是
115c: 8b 45 f8 mov -0x8(%rbp),%eax // %eax = 3
115f: 8d 50 01 lea 0x1(%rax),%edx // %edx = 4
1162: 89 55 f8 mov %edx,-0x8(%rbp) // i = 4
将 i 变量的值从内存(-0x8(%rbp))放入寄存器 %eax,然后在寄存器中增加1,将值放入%edx,然后再将%edx中的值放入内存(4)。这里%eax保存了原始的值3,是加法操作的一个操作数,同时内存的值更新。
对于++ i,是怎么做的呢?
1160: 83 45 f8 01 addl $0x1,-0x8(%rbp) // i = 5
这里就比较简单粗暴,直接在内存位置进行了加1。
后面在进行加法操作的时候,左操作数已经准备好了(在%eax中),然后右操作数再从内存进入寄存器,此时的值就是5。所以3+5=8。这里要特别强调的一点,++ i 不是自己一遍做好就取出来存到寄存器,而是会等到所有的对 i 的操作都结束,在必须使用它来进行操作的时候才从内存中取值,所以它的值是 i 的最新值,而不一定是 ++ i之后的值。
为了说明这个问题,我们再来看一个例子。
#include <stdio.h>
int main(){
int i = 3;
int sum;
sum = ++i + i++;
printf("sum is %d.\n",sum);
return 0;
}
大家觉得这个结果是多少呢?(答案是9)
++ i 之后变成4, i ++ 把这个i的值4取出来,放到寄存器中,然后i ++,i的值变成了5,再写回内存,当要进行加法操作的时候,就要取 i 的值,此时i的值为现在内存中的值,所以++ i 变成了5,而i ++ 是4,故sum = 5 + 4 = 9
还是看一下汇编代码比较清晰
0000000000001149 <main>:
1149: f3 0f 1e fa endbr64
114d: 55 push %rbp
114e: 48 89 e5 mov %rsp,%rbp
1151: 48 83 ec 10 sub $0x10,%rsp
1155: c7 45 f8 03 00 00 00 movl $0x3,-0x8(%rbp) // i = 3
115c: 83 45 f8 01 addl $0x1,-0x8(%rbp) // i = 4
1160: 8b 45 f8 mov -0x8(%rbp),%eax // %eax = 4
1163: 8d 50 01 lea 0x1(%rax),%edx // %edx = 5
1166: 89 55 f8 mov %edx,-0x8(%rbp) // i = 5
1169: 8b 55 f8 mov -0x8(%rbp),%edx // %edx = 5
116c: 01 d0 add %edx,%eax // %eax = 9
116e: 89 45 fc mov %eax,-0x4(%rbp) // sum = 9
1171: 8b 45 fc mov -0x4(%rbp),%eax
1174: 89 c6 mov %eax,%esi
1176: 48 8d 05 87 0e 00 00 lea 0xe87(%rip),%rax # 2004 <_IO_stdin_used+0x4>
117d: 48 89 c7 mov %rax,%rdi
1180: b8 00 00 00 00 mov $0x0,%eax
1185: e8 c6 fe ff ff call 1050 <printf@plt>
118a: b8 00 00 00 00 mov $0x0,%eax
118f: c9 leave
1190: c3 ret
从这个例子,我们可以看出来什么呢?语言的特点其实是有编译的实现来决定的。比如,我们计算++ i 的时候,就是要先改它的值,并且参与计算的时候,我一定要取到它的最新值。
【注意与i ++的区别,一个是取它原来的值(i ++),一个是取它最新的值(++ i)】
我们学习语言的时候如果主动地去想一想底层的特点,那么对语言特性的理解会更深刻。如果能真正理解编译器怎么处理++i,那么对上面例子的结果应该也能理解了。