从汇编角度来看解引用与自增
引言
根据这个帖子:https://stackoverflow.com/questions/859770/post-increment-on-a-dereferenced-pointer 。
*ptr++背后的原理:
- 保存当前的ptr
- 自增ptr
- 对步骤1保存的ptr进行解引用,此时得到表达式的最终值
于是,对于*ptr++可以等价理解为下面的代码:
1 | *ptr; |
C++操作符的优先级:

初探普通指针
举个例子,考虑下面代码:
1 | int main() |
结果为:
1 | 5 |
因为ptr是一个指针,它存储指向的对象的地址,令该地址+1,再获取这个地址的对象的值,因为我们并没有在这个地址存储对象,所以别指望输出什么有效的信息
那如果用括号括起来呢?
1 | int main() |
结果还是:
1 | 5 |
继续,考虑*++ptr与++*ptr:
1 | int main() |
结果为:
1 | 2293308 |
可以看到,*++ptr1遵循从右往左的结合律(associativity),先对ptr1进行自增,然后再解引用,这里我们没有存储对象。++*ptr2同样遵循从右往左的结合律,先对ptr2进行解引用,即得到变量i,再对i进行自增,得到6。于是,最后一行输出变量i时,输出为6。
再探迭代器
迭代器是STL中经常使用的,我们来看看迭代器的解引用与自增操作是否与普通指针一样:
1 | int main() |
结果如下,可以看到,迭代器与普通指针表现一致
1 | 10 |
深入普通指针的汇编实现
*ptr的汇编实现
正好最近看完了CSAPP这本书,虽然本科在微机原理中学过一些汇编,但那些应试教育学过就忘得差不多了,看完CSAPP后,我对汇编语言的价值有了新的认识,于是考虑用反汇编的手段详细探索一下*ptr++底层的汇编实现。
我编写了两个cpp文件,
1 | //helloworld.cpp |
GCC可以帮我们得到汇编代码:
1 | gcc -c -S helloworld.cpp |
得到helloworld.s与helloworld_copy .s这两个汇编文件,为了对比不同,可以用IDE支持的compare功能,这里我就用VSCODE的compare功能。

主要看两个汇编文件不同的地方:
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 | //helloworld.cpp |
compare汇编结果如下,输出到cout其实就是把变量i传送到寄存器eax中:
再看一看表达式*ptr的汇编实现,在helloworld.cpp中,cout输出*ptr,汇编代码如下:
1 | movl $5, -12(%rbp) |
(%rax)表示加载寄存器rax中地址所指的内容,这与C++代码一致,因为指针ptr本身存储在地址-8%(rbp),即存储在rax中,所以对指针ptr解引用,就相当于获得rax所指向的地址的内容。
*ptr++的汇编实现
我们对比看一看i++与*ptr++的汇编实现,假设两个汇编文件各自输出这两个表达式,汇编代码如下:

先看左边,这是输出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.cpp中cout输出的是++i

左边的++i的汇编实现是显而易见的,用了addl把立即数1加到i上面,最后输出到cout。
右边的++*ptr的汇编实现有点繁琐,我们一步步来看
- 第22-25行与前文一致,变量
i存储在-12(%rbp),指针ptr存储在地址-8%(rbp),即rax中 - 第26行,把
rax所指地址的值写入eax,这时eax中的值为5,这里有个知识点不能忽略:eax是rax的低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.cpp中cout输出的是*++ptr

直接看右边,第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