Compiler- 自增运算

2023-11-19

我们来看一下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,那么对上面例子的结果应该也能理解了。


以上为中科大软件学院《编译工程》课后总结,感谢郭燕老师的倾心教授,老师讲的太好啦(^_^) 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Compiler- 自增运算 的相关文章

随机推荐