关于 i++
操作总会在一些意想不到的地方给程序员带来各种各样的麻烦。例如:多次执行count = count ++
后,为什么 count
的值不变? 要深入理解这个操作,就需要从更深层次的汇编来解释它了。
1. 准备工作
在 Linux 64 位机器下编写 c++
代码:
int main()
{
int i = 0;
i++;
cout << i;
}
使用 g++ -g 1.cpp
对其进行编译并加入调试信息。
2. 分析汇编指令
使用 objdum -S a.out
可以将汇编代码与调试信息打开。找到 main
函数开始的地方:
int main()
{
400756: 55 push %rbp
400757: 48 89 e5 mov %rsp,%rbp
40075a: 48 83 ec 10 sub $0x10,%rsp
int i = 0;
40075e: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
i++;
400765: 83 45 fc 01 addl $0x1,-0x4(%rbp)
cout << i;
400769: 8b 45 fc mov -0x4(%rbp),%eax
40076c: 89 c6 mov %eax,%esi
40076e: bf 60 10 60 00 mov $0x601060,%edi
400773: e8 88 fe ff ff callq 400600 <_ZNSolsEi@plt>
}
这些都是 AT&T 汇编,想要深入了解就得参考 GUN AS 手册。在此不赘述。可见,i++
操作被翻译为:
addl $0x1,-0x4(%rbp)
也就是,将 rpb-4 内存地址中的值加1。很简单,没啥问题。
3.复杂情况
当然了,单步执行 i++
操作翻译成汇编指令看起来很简单,但是当组合i++
操作在一些语句中,例如:
int main()
{
int i = 1;
int count = 0;
for(i = 0; i < 10; i++)
{
count = count++;
}
cout << count;
}
这时候的汇编指令又会是怎么样的?如法泡制,看一看汇编指令:
using namespace std;
int main()
{
400756: 55 push %rbp
400757: 48 89 e5 mov %rsp,%rbp
40075a: 48 83 ec 10 sub $0x10,%rsp
int i = 1;
40075e: c7 45 f8 01 00 00 00 movl $0x1,-0x8(%rbp)
int count = 0;
400765: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
for(i = 0; i < 10; i++)
40076c: c7 45 f8 00 00 00 00 movl $0x0,-0x8(%rbp)
400773: eb 10 jmp 400785 <main+0x2f>
{
count = count++;
400775: 8b 45 fc mov -0x4(%rbp),%eax
400778: 8d 50 01 lea 0x1(%rax),%edx
40077b: 89 55 fc mov %edx,-0x4(%rbp)
40077e: 89 45 fc mov %eax,-0x4(%rbp)
using namespace std;
int main()
{
int i = 1;
int count = 0;
for(i = 0; i < 10; i++)
400781: 83 45 f8 01 addl $0x1,-0x8(%rbp)
400785: 83 7d f8 09 cmpl $0x9,-0x8(%rbp)
400789: 7e ea jle 400775 <main+0x1f>
{
count = count++;
}
cout << count;
40078b: 8b 45 fc mov -0x4(%rbp),%eax
40078e: 89 c6 mov %eax,%esi
400790: bf 60 10 60 00 mov $0x601060,%edi
400795: e8 66 fe ff ff callq 400600 <_ZNSolsEi@plt>
}
可以看到,仅仅增加了一个循环,汇编代码就变得非常有意思了。一步步分析。
1. 定义变量
movl $0x1,-0x8(%rbp) //分配变量 i 的空间
movl $0x0,-0x4(%rbp) //分配变量 count 的空间
可以看到,先定义的变量被分到了低地值区域。
2. 开始循环
movl $0x0,-0x8(%rbp)
jmp 400785 <main+0x2f>
接着是 for 循环中的第一条语句,即 i = 0
,然后跳至循环主体 <main+0x2f>。
3. 循环体
cmpl $0x9,-0x8(%rbp)
jle 400775 <main+0x1f>
上面两句用于判断循环是否结束,若未结束,继续执行:
mov -0x4(%rbp),%eax // 将 count 的地址放在 eax 中
lea 0x1(%rax),%edx // 从 eax 存放的地址取值加 1,放在 edx 中
mov %edx,-0x4(%rbp) // 将 edx 值赋给 count
mov %eax,-0x4(%rbp) // 将 eax 值赋给 count
这几句汇编就是 count = count++
了。可以看到,在最后一步中将 +1 的结果覆盖掉了。也就是说,i++ 操作需要将原内容拷贝一份。
因此,实际上count = count + 1
等价于:
int tmp1 = count;
int tmp2 = tmp + 1;
count = tmp2;
count = tmp1;
3. 从运算符角度看
感谢这篇文章 。其实,产生上面汇编代码的根本原因在于 c++ 对++
操作的定义。
Operator Operator::operator++()
{
++value; //内部成员变量
return *this;
}
Operator Operator::operator++(int)
{
Operator temp;
temp.value=value;
value++;
return temp;
}
可以看到,++
操作返回的是原值。所以,count = count ++
可以看作:
int ori_count = count;
count = count + 1;
count = ori_count;
这样,就解开了 count
值不变之迷。