引言
最近看到*ptr++
,不是很理解,遂上StackOverflow查询,找到一个帖子:https://stackoverflow.com/questions/859770/post-increment-on-a-dereferenced-pointer 。
其中有人给出了*ptr++
背后的原理:
- 保存当前的ptr
- 自增ptr
- 对步骤1保存的ptr进行解引用,此时得到表达式的最终值
于是,对于*ptr++
可以等价理解为下面的代码:
*ptr;
ptr = ptr + 1;
顺便复习一下C++操作符的优先级:
c++operatorpriority.png初探普通指针
举个例子,考虑下面代码:
int main()
{
int i = 5, *ptr = &i;
cout << *ptr++ << endl;;
cout << *ptr << endl;
}
结果为:
5
2293320
因为ptr
是一个指针,它存储指向的对象的地址,令该地址+1,再获取这个地址的对象的值,因为我们并没有在这个地址存储对象,所以别指望输出什么有效的信息
那如果用括号括起来呢?
int main()
{
int i = 5, *ptr = &i;
cout << *(ptr++) << endl;;
}
结果还是:
5
2293320
继续,考虑*++ptr
与++*ptr
:
int main()
{
int i = 5, *ptr1 = &i, *ptr2 = &i;
cout << *++ptr1 << endl;
cout << ++*ptr2 << endl;
cout << i << endl;
}
结果为:
2293308
6
6
可以看到,*++ptr1
遵循从右往左的结合律(associativity),先对ptr1
进行自增,然后再解引用,这里我们没有存储对象。++*ptr2
同样遵循从右往左的结合律,先对ptr2
进行解引用,即得到变量i
,再对i
进行自增,得到6。于是,最后一行输出变量i
时,输出为6。
再探迭代器
迭代器是STL中经常使用的,我们来看看迭代器的解引用与自增操作是否与普通指针一样:
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;
}
结果如下,可以看到,迭代器与普通指针表现一致
10
10
20
30
30
31
31
深入普通指针的汇编实现
*ptr的汇编实现
正好最近看完了CSAPP这本书,虽然本科在微机原理中学过一些汇编,但那些应试教育学过就忘得差不多了,看完CSAPP后,我对汇编语言的价值有了新的认识,于是考虑用反汇编的手段详细探索一下*ptr++
底层的汇编实现。
我编写了两个cpp文件,
//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可以帮我们得到汇编代码:
$ gcc -c -S helloworld.cpp
$ gcc -c -S helloworld_copy.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
:
//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
中:
再看一看表达式*ptr
的汇编实现,在helloworld.cpp
中,cout
输出*ptr
,汇编代码如下:
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++
的汇编实现,假设两个汇编文件各自输出这两个表达式,汇编代码如下:
先看左边,这是输出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.
网友评论