引言

根据这个帖子:https://stackoverflow.com/questions/859770/post-increment-on-a-dereferenced-pointer

*ptr++背后的原理:

  1. 保存当前的ptr
  2. 自增ptr
  3. 对步骤1保存的ptr进行解引用,此时得到表达式的最终值

于是,对于*ptr++可以等价理解为下面的代码:

1
2
*ptr;
ptr = ptr + 1;

C++操作符的优先级:

c++operatorpriority.png

初探普通指针

举个例子,考虑下面代码:

1
2
3
4
5
6
int main()
{
int i = 5, *ptr = &i;
cout << *ptr++ << endl;;
cout << *ptr << endl;
}

结果为:

1
2
5
2293320

因为ptr是一个指针,它存储指向的对象的地址,令该地址+1,再获取这个地址的对象的值,因为我们并没有在这个地址存储对象,所以别指望输出什么有效的信息

那如果用括号括起来呢?

1
2
3
4
5
int main()
{
int i = 5, *ptr = &i;
cout << *(ptr++) << endl;;
}

结果还是:

1
2
5
2293320

继续,考虑*++ptr++*ptr

1
2
3
4
5
6
7
int main()
{
int i = 5, *ptr1 = &i, *ptr2 = &i;
cout << *++ptr1 << endl;
cout << ++*ptr2 << endl;
cout << i << endl;
}

结果为:

1
2
3
2293308
6
6

可以看到,*++ptr1遵循从右往左的结合律(associativity),先对ptr1进行自增,然后再解引用,这里我们没有存储对象。++*ptr2同样遵循从右往左的结合律,先对ptr2进行解引用,即得到变量i,再对i进行自增,得到6。于是,最后一行输出变量i时,输出为6。

再探迭代器

迭代器是STL中经常使用的,我们来看看迭代器的解引用与自增操作是否与普通指针一样:

1
2
3
4
5
6
7
8
9
10
11
12
int main()
{
vector<int> vec = {10, 20, 30, 40};
auto it = vec.begin();
cout << *it << endl;
cout << *it++ << endl;
cout << *it << endl;
cout << *++it << endl;
cout << *it << endl;
cout << ++*it << endl;
cout << *it << endl;
}

结果如下,可以看到,迭代器与普通指针表现一致

1
2
3
4
5
6
7
10
10
20
30
30
31
31

深入普通指针的汇编实现

*ptr的汇编实现

正好最近看完了CSAPP这本书,虽然本科在微机原理中学过一些汇编,但那些应试教育学过就忘得差不多了,看完CSAPP后,我对汇编语言的价值有了新的认识,于是考虑用反汇编的手段详细探索一下*ptr++底层的汇编实现。

我编写了两个cpp文件,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//helloworld.cpp
#include <iostream>
using namespace std;

int main()
{
int i = 5, *ptr = &i;
}

//helloworld_copy.cpp
#include <iostream>
using namespace std;

int main()
{
int i = 5;
}

GCC可以帮我们得到汇编代码:

1
2
$ gcc -c -S helloworld.cpp
$ gcc -c -S helloworld_copy.cpp

得到helloworld.shelloworld_copy .s这两个汇编文件,为了对比不同,可以用IDE支持的compare功能,这里我就用VSCODE的compare功能

helloworldcompare.png

主要看两个汇编文件不同的地方:

  • movl传送双字
    • helloworld_copy把立即数5($5)送到地址-4(%rbp)处,这样我们可以说变量i存储在-4(%rbp)
    • helloworld把立即数5($5)送到地址-12(%rbp)处,这样我们可以说变量i存储在-12(%rbp)
  • leaq取出有效地址,注意它不会引用内存,而仅仅是将有效地址写入到目的寄存器里面,在右边的第23行,就是把变量i的地址加载到寄存器rax中,于是rax相当于一个指向i的指针
  • movq传送四字,把寄存器rax的内容传送到地址-8%(rbp),别忘了rax存储的是变量i的地址,所以我们可以说指针ptr存储在地址-8%(rbp)
  • -12(%rbp)-8%(rbp)差了4,正好是int型变量在64位操作系统中所占空间大小

如果为两个cpp文件中变量i输出到cout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//helloworld.cpp
#include <iostream>
using namespace std;

int main()
{
int i = 5, *ptr = &i;
cout << i;
}

//helloworld_copy.cpp
#include <iostream>
using namespace std;

int main()
{
int i = 5;
cout << i;
}

compare汇编结果如下,输出到cout其实就是把变量i传送到寄存器eax中:
coutcompare.png

再看一看表达式*ptr的汇编实现,在helloworld.cpp中,cout输出*ptr,汇编代码如下:

1
2
3
4
5
movl	$5, -12(%rbp)
leaq -12(%rbp), %rax
movq %rax, -8(%rbp)
movq -8(%rbp), %rax
movl (%rax), %eax

(%rax)表示加载寄存器rax中地址所指的内容,这与C++代码一致,因为指针ptr本身存储在地址-8%(rbp),即存储在rax中,所以对指针ptr解引用,就相当于获得rax所指向的地址的内容。

*ptr++的汇编实现

我们对比看一看i++*ptr++的汇编实现,假设两个汇编文件各自输出这两个表达式,汇编代码如下:

inccompare.png

先看左边,这是输出i++的汇编实现,第24行是最迷惑的,我们可以反推一下

  • 第26行,cout输出的还是在第23行得到的寄存器eax中的值
  • 第24行,leal计算%rax+1然后保存在edx
  • 第25行,edx中的值应该是6,于是反推第24行的%rax+1应该是6,于是rax中的值应该是5,而前文并没有出现rax所以猜想rax就是与地址-4(%rbp)所存储内容相同(如果有错麻烦告知一下)

再看右边,这是输出*ptr++的汇编实现

  • 第22行,变量i的值为5,存储在-12(%rbp)
  • 第23行,将变量i的地址写入rax
  • 第24行,将rax的值写入地址-8(%rbp)中,这个就是指针ptr的所在之处
  • 第26行,将%rax+4的结果写入rdx,因为rax中的值就是变量i的地址,所以相当于把地址+4后写入rdx,那为什么指针+1会导致地址+4?我的环境是64位操作系统,一个int*型变量应该是8个字节,好像应该是地址+8才对?但是这个地址存储的是int型变量,在64位OS中占4字节,所以指针+1就是地址+4,没毛病
  • 第27行,将rdx的值写入地址-8(%rbp),这是指针ptr得到更新后的值
  • 第29行,cout输出的是第28行寄存器rax所指的地址的值,而rax是在第25行得到,这在第26自行增操作之前,所以输出仍为5

总结:以上分析正好与StackOverflow中的回答相一致,*ptr++先存储指针,再让指针加一,最后再解引用之前存储的那个指针,表达式最后的结果是指针原来所指对象

++*ptr的汇编实现

接下来看一看++*ptr的汇编实现,为了对比,helloworld_copy.cppcout输出的是++i

preinccompare.png

左边的++i的汇编实现是显而易见的,用了addl把立即数1加到i上面,最后输出到cout

右边的++*ptr的汇编实现有点繁琐,我们一步步来看

  • 第22-25行与前文一致,变量i存储在-12(%rbp),指针ptr存储在地址-8%(rbp),即rax
  • 第26行,把rax所指地址的值写入eax,这时eax中的值为5,这里有个知识点不能忽略:eaxrax的低32位寄存器,int型变量在64位OS中占4字节,所以推测rax的值也变成5
  • 第27行,将%rax+1的结果写入edx,推测成立,此时edx的值为6
  • 第28行,将地址-8(%rbp)中的值写入rax中,所以rax存储变量i的地址,此时rax又变回指针ptr
  • 第29行,将edx中的值写入到rax所指向的地址中,即写入指针ptr中,至此,变量i获得自增
  • 第30-32,将地址-8%(rbp)所存内容输出到cout

反推确认一下,我们已知最后输出的是6,所以第31行eax为6, 所以第30行rax所指的地址的值为6, 第30行地址-8%(rbp)所存储的是一个地址,地址中的值为6,第29行更新了rax所指向的地址的值,这也就更新了地址-8%(rbp)所指向的地址的值,所以反推得到第29行的edx的值为6,而edx来自于第27行的leal操作,所以leal操作就是自增操作

总结:++*ptr先解引用指针,获取指针所指的对象,再令该对象+1,表达式最后的结果是自增后的对象

*++ptr的汇编实现

仿照前面的操作,不同的是helloworld.cppcout输出的是*++ptr

starincptr.png

直接看右边,第22-24与前文一致,第25行将立即数4加在了地址-8(%rbp)中的值上,从第23、24行可知地址-8(%rbp)中本来存的是地址-12(%rbp),于是这就相当于地址+4,第26、27行就是解引用这个加4后的地址,明显这是个无效地址,所以输出的是2293320这种毫无意义的值

总结:*++ptr先自增指针,然后解引用指针获取指针所指的对象,表达式最后的结果是指针自增后所指的对象

关于自增的小tips

it++++it均可的情况下,用++it,可提高非常微小的性能

it++ returns a copy of the previous iterator. Since this iterator is not used, this is wasteful. ++it returns a reference to the incremented iterator, avoiding the copy.

原文发表于:https://www.jianshu.com/p/78188418166e by 2019.12.10 23:43:02