i++

关于 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 值不变之迷。

感谢稀稀拉拉的赞赏