从汇编的角度理解什么是引用
C++中引入的引用类型,给我们带来了很大的方便。通过向函数传递引用,我们既可以享受像传递指针一样直接修改变量值的优点,又避免了空指针和野指针造成的问题。在日常开发中我们应该尽量使用引用,避免使用指针。但是引用到底是什么,看起来好像引用跟指针有着千丝万缕的联系,同时两者又有很大的差别,那么引用跟指针到底是什么关系呢?教材上通常会说,引用就是变量的别名,但是光看这句话可能还是不太明白引用的本质。其实按照我的理解引用可以看做一种特殊的指针,在这里做一个总结。
0.指针和引用的区别
指针和引用的区别主要有以下几点:
- 指针可以先定义后绑定到(指向)某个对象,并且可以置为NULL;引用必须在定义的时候绑定到某对象。
- 指针可以改变指向的对象,引用在不能改变绑定的对象。(有没有觉得1、2两个特点跟const指针很像?)
- 通过引用可以像被绑定的对象本身一样操作,指针不可以。
- 对指针进行sizeof操作得到的是指针本身占用的内存大小,32位系统是4字节,64位系统是8个字节; 对引用进行sizeof操作得到的是被绑定到的变量占用的内存大小。
- 指针可以有二级、三级等多级指针,引用没有。
1.函数传引用参数传递的是什么?
函数传指针参数相信大家都很清楚。那么函数传引用参数,到底传递的是什么呢?来看一个例子:
#include<iostream>
void passByRefrence(int &a, int &b);
using namespace std;
int main()
{
int a = 1, b = 2;
printf("%p %p\n", &a, &b);
passByRefrence(a, b);
return 0;
}
void passByRefrence(int &a, int &b)
{
printf("%p %p\n", &a, &b);
}
这段代码的执行结果是:
$ ./a.out
000000000061fe4c 000000000061fe48
000000000061fe4c 000000000061fe48
从结果可以看出来,在passByRefrence
函数中的a,b变量地址和main函数中定义的a,b变量地址是一样的,貌似传引用就是传递的变量地址。也许上面的结果不能让你信服,那么我们看一个更有说服力的例子。
#include<iostream>
using namespace std;
int add(int *a, int *b)
{
return *a + *b;
}
int add(int &a, int &b)
{
return a + b;
}
int main()
{
int a = 2, b = 3;
add(a, b);
add(&a, &b);
return 0;
}
我们把这段代码编译后得到的可执行文件进行反编译一下看看结果:
TIPS:反编译结果可以用objdump -d a.exe > a.s得到
反编译得到的文件很长,在这里我们只截取两个add函数反编译的结果
// int add(int *a, int *b);
000000000040164e <_Z3addPiS_>:
40164e: 55 push %rbp
40164f: 48 89 e5 mov %rsp,%rbp
401652: 48 89 4d 10 mov %rcx,0x10(%rbp)
401656: 48 89 55 18 mov %rdx,0x18(%rbp)
40165a: 48 8b 45 10 mov 0x10(%rbp),%rax
40165e: 8b 10 mov (%rax),%edx
401660: 48 8b 45 18 mov 0x18(%rbp),%rax
401664: 8b 00 mov (%rax),%eax
401666: 01 d0 add %edx,%eax
401668: 5d pop %rbp
401669: c3 retq
// int add(int &a, int &b)
000000000040166a <_Z3addRiS_>:
40166a: 55 push %rbp
40166b: 48 89 e5 mov %rsp,%rbp
40166e: 48 89 4d 10 mov %rcx,0x10(%rbp)
401672: 48 89 55 18 mov %rdx,0x18(%rbp)
401676: 48 8b 45 10 mov 0x10(%rbp),%rax
40167a: 8b 10 mov (%rax),%edx
40167c: 48 8b 45 18 mov 0x18(%rbp),%rax
401680: 8b 00 mov (%rax),%eax
401682: 01 d0 add %edx,%eax
401684: 5d pop %rbp
401685: c3 retq
对比两段代码可以看到,两个add
函数的汇编代码一模一样,没有任何区别。也就是说传指针和传引用在汇编层面上的实现是一样的,传引用就相当于传指针。第二段代码是add
函数传引用版本的实现,rbp + 0x10
位置存储的是变量a的内存地址,rbp + 0x18
位置存储的是变量b的内存地址。
2. 引用是否占用内存?
这个问题貌似有点奇怪,引用是变量的别名,怎么会占用内存呢?说不清楚,还是看看汇编代码吧!我们把add
函数的传引用版本修改一下:
int add(int &a, int &b)
{
int &p = a;
return p + b;
}
重新反编译得到add
函数的反汇编实现,为了方便对比,我把上一个版本也放在这里 :
// 版本1
000000000040166a <_Z3addRiS_>:
40166a: 55 push %rbp
40166b: 48 89 e5 mov %rsp,%rbp
40166e: 48 89 4d 10 mov %rcx,0x10(%rbp)
401672: 48 89 55 18 mov %rdx,0x18(%rbp)
401676: 48 8b 45 10 mov 0x10(%rbp),%rax
40167a: 8b 10 mov (%rax),%edx
40167c: 48 8b 45 18 mov 0x18(%rbp),%rax
401680: 8b 00 mov (%rax),%eax
401682: 01 d0 add %edx,%eax
401684: 5d pop %rbp
401685: c3 retq
// 版本2
000000000040166a <_Z3addRiS_>:
40166a: 55 push %rbp
40166b: 48 89 e5 mov %rsp,%rbp
40166e: 48 83 ec 10 sub $0x10,%rsp // 分配16字节的内存
401672: 48 89 4d 10 mov %rcx,0x10(%rbp)
401676: 48 89 55 18 mov %rdx,0x18(%rbp)
40167a: 48 8b 45 10 mov 0x10(%rbp),%rax
40167e: 48 89 45 f8 mov %rax,-0x8(%rbp) // 保存变量a
401682: 48 8b 45 f8 mov -0x8(%rbp),%rax
401686: 8b 10 mov (%rax),%edx
401688: 48 8b 45 18 mov 0x18(%rbp),%rax
40168c: 8b 00 mov (%rax),%eax
40168e: 01 d0 add %edx,%eax
401690: 48 83 c4 10 add $0x10,%rsp
401694: 5d pop %rbp
401695: c3 retq
对比两个版本的汇编实现,可以看到第二个版本多了几条指令。第3条指令sub $0x10, %rsp
将栈指针减小16,也就是在栈中分配了16个字节的内存(其实8个字节已经足够,但是为了内存对齐,申请了16个字节内存,这个暂不讨论)。第6条指令将rbp + 0x10
内存的内容送入rax,这个内容也就是变量a的地址,第7条指令将寄存器rax的值送入rbp - 0x8
位置,也就是说这个内存位置保存了变量a的地址,rbp - 0x8
其实就是引用p。从这个结果可以看出来,引用保存的就是地址,是需要占用内存的。
THE END