i++和++i是C语言入门最基础的一个知识,以前学的时候,仅仅是记住,i++是先赋值再加1,++i是先相加后赋值。前段时间在一个循环buffer的读写指针访问时,不小心使用了i++导致,程序一直报段错误,由此我试着从更底层的角度来理解下i++和++i。
🎬个人简介:一个全栈工程师的升级之路!
📋个人专栏:C/C++精进之路
🎀CSDN主页 发狂的小花
🌄人生秘诀:学习的本质就是极致重复!
目录
1 用几个例子详细看i++和++i的区别
第一步:
这是有i++和++i的C代码
第二步:
使用gcc -S func_call.c -o func_call.s 得到汇编代码
![](https://img-blog.csdnimg.cn/direct/2f8edae2e4314a5db7af6100955489b1.png)
![](https://img-blog.csdnimg.cn/direct/70bf153b701b472bad3d5ca4cdb2c5fe.png)
第三步:
逐行解释i++的汇编代码(如下):
1. `.file "func_call.c"`:指定当前文件名为"func_call.c"。
2. `.text`:表示接下来的代码是文本段,即程序的可执行部分。
3. `.globl main`:声明全局变量main,使得其他文件中可以调用这个函数。
4. `.type main, @function`:设置main函数的类型为函数。
5. `main:`:开始定义main函数。
6. `.LFB0:`:局部帧缓冲区(Local Frame Buffer)的起始地址。
7. `.cfi_startproc`:表示开始处理过程。
8. `pushq %rbp`:将基址指针寄存器rbp压入栈中。
9. `.cfi_def_cfa_offset 16`:设置当前帧的基址偏移量为16字节。
10. `.cfi_offset 6, -16`:设置栈顶元素相对于rbp的偏移量为-16字节。
11. `movq %rsp, %rbp`:将栈顶指针赋值给基址指针寄存器rbp。
12. `.cfi_def_cfa_register 6`:设置当前帧的基址寄存器为rbp。
13. `movl $0, -8(%rbp)`:将0赋值给rbp-8的位置。
14. `movl -8(%rbp), %eax`:将rbp-8位置的值赋给eax寄存器。
15. `leal 1(%rax), %edx`:将rax寄存器的值加1后的结果赋给edx寄存器。
16. `movl %edx, -8(%rbp)`:将edx寄存器的值赋给rbp-8的位置。
17. `movl %eax, -4(%rbp)`:将eax寄存器的值赋给rbp-4的位置。
18. `movl $0, %eax`:将0赋值给eax寄存器。
19. `popq %rbp`:将栈顶元素弹出并赋值给基址指针寄存器rbp。
20. `.cfi_def_cfa 7, 8`:设置当前帧的基址寄存器为rbp,基址偏移量为8字节。
21. `ret`:返回到调用者。
22. `.cfi_endproc`:表示过程处理结束。
23. `.LFE0:`:局部帧缓冲区(Local Frame Buffer)的结束地址。
24. `.size main, .-main`:设置main函数的大小为从开始到结束的距离。
25. `.ident "GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0"`:标识编译器信息。
26. `.section .note.GNU-stack,"",@progbits`:定义一个名为.note.GNU-stack的节,用于存储栈的信息。
重点关注13~19行:
13行:对应c代码 int i = 0; 将0赋值给栈帧中的rbp-8的位置,由于该段代码有两个int类型局部变
量,因此需要8个字节存储局部变量,rbp-8就是栈顶指针偏移8个字节,栈是由高到底的,
因此是减去8,即i变量在栈帧的首地址为rbp-8
14行: 将rbp-8位置的值赋给eax寄存器,将i给累加器eax,eax中的值为0
15行:得到edx寄存器的值为1
16行:将edx中的值1赋值给栈帧中的rbp-8,完成i++,此时i = 1;
17行:将eax中的值0赋值为栈帧中的rbp-4位置,即a = 0;
18行:将0赋值给eax寄存器,eax一般用来存放函数的返回值
19行:栈顶元素出栈rbp-8存储i++后的i = 1,rbp-4存储a = 0。
由此可以得到int a = i++;中的a = 0;
逐行解释++i的汇编代码(如下):
1. `.file "func_call.c"`:这是指定当前文件名的指令,表示这个文件的名字是"func_call.c"。
2. `.text`:这是一个段描述符,表示接下来的代码是程序的文本部分。
3. `.globl main`:这是一个全局变量声明,表示main函数在其他地方也可以被调用。
4. `.type main, @function`:这是一个类型声明,表示main函数的类型是函数。
5. `main:`:这是main函数的开始。
6. `.LFB0:`:这是一个局部帧缓冲区(Local Frame Buffer)的开始地址。
7. `.cfi_startproc`:这是一个控制流指示器(Control-Flow Indirection),表示开始处理过程。
8. `pushq %rbp`:将基址指针寄存器rbp的值压入栈中。
9. `.cfi_def_cfa_offset 16`:设置当前帧的基址偏移量为16字节。
10. `.cfi_offset 6, -16`:设置栈顶元素相对于rbp的偏移量为-16字节。
11. `movq %rsp, %rbp`:将栈顶指针赋值给基址指针寄存器rbp。
12. `.cfi_def_cfa_register 6`:设置当前帧的基址寄存器为rbp。
13. `movl $0, -8(%rbp)`:将0赋值给rbp-8的位置。
14. `addl $1, -8(%rbp)`:将rbp-8位置的值加1。
15. `movl -8(%rbp), %eax`:将rbp-8位置的值赋给eax寄存器。
16. `movl %eax, -4(%rbp)`:将eax寄存器的值赋给rbp-4的位置。
17. `movl $0, %eax`:将0赋值给eax寄存器。
18. `popq %rbp`:将栈顶元素弹出并赋值给基址指针寄存器rbp。
19. `.cfi_def_cfa 7, 8`:设置当前帧的基址寄存器为rbp,基址偏移量为8字节。
20. `ret`:返回到调用者。
21. `.cfi_endproc`:表示过程处理结束。
22. `.LFE0:`:这是一个局部帧缓冲区(Local Frame Buffer)的结束地址。
23. `.size main, .-main`:设置main函数的大小为从开始到结束的距离。
24. `.ident "GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0"`:这是编译器的信息。
25. `.section .note.GNU-stack,"",@progbits`:这是一个节描述符,表示接下来的代码是程序的数据部分。
重点关注13~18行:
13行:对应c代码 int i = 0; 将0赋值给栈帧中的rbp-8的位置,由于该段代码有两个int类型局部变
量,因此需要8个字节存储局部变量,rbp-8就是栈顶指针偏移8个字节,栈是由高到底的,
因此是减去8,即i变量在栈帧的首地址为rbp-8
14行: 将rbp-8位置的值加1,此时 i = 1;
15行:将rbp-8位置的值给eax寄存器,eax值为1
16行:将eax中的值给rbp-4处位置,即 int a = 1;
17行:将0赋值给eax寄存器,eax一般用来存放函数的返回值
18行:栈顶元素出栈rbp-8存储i++后的i = 1,rbp-4存储a = 1。
由此可以得到 int a = ++i;中a的值为1
如果给定这样的c代码和它的汇编代码:
由上述我们可以更清晰的看到栈帧中rbp-4位置,即a的值的变化
2 i++和++i的理解
i++的原理是先将局部变量表的变量入栈,然后局部变量本身自增,接着将栈顶的操作数保存到局部变量表(也就是覆盖自增操作),其结果是自增的结果被还原了;
++i的原理是上来就将局部变量表的变量自增,然后入栈,接着不管在栈顶做什么操作,都是基于自增后的值来操作的,而i++都是基于自增前的值来操作的。
由于函数在调用过程中局部变量都是在栈中取的,因此会出现i++是先赋值再自加的现象,++i会出现直接自加后赋值的现象。
写完了,突然觉得这玩意没什么深层次的原理,就是纯粹的语言设计和编译器设计成这样。
🌈我的分享也就到此结束啦🌈
如果我的分享也能对你有帮助,那就太好了!
若有不足,还请大家多多指正,我们一起学习交流!
📢未来的富豪们:点赞👍→收藏⭐→关注🔍
感谢大家的观看和支持!最后,☺祝愿大家每天有钱赚!!!