C++中引入的引用类型,给我们带来了很大的方便。通过向函数传递引用,我们既可以享受像传递指针一样直接修改变量值的优点,又避免了空指针和野指针造成的问题。在日常开发中我们应该尽量使用引用,避免使用指针。但是引用到底是什么,看起来好像引用跟指针有着千丝万缕的联系,同时两者又有很大的差别,那么引用跟指针到底是什么关系呢?教材上通常会说,引用就是变量的别名,但是光看这句话可能还是不太明白引用的本质。其实按照我的理解引用可以看做一种特殊的指针,在这里做一个总结。
0.指针和引用的区别
指针和引用的区别主要有以下几点:
- 指针可以先定义后绑定到(指向)某个对象,并且可以置为NULL;引用必须在定义的时候绑定到某对象。
- 指针可以改变指向的对象,引用在不能改变绑定的对象。(有没有觉得1、2两个特点跟const指针很像?)
- 通过引用可以像被绑定的对象本身一样操作,指针不可以。
- 对指针进行sizeof操作得到的是指针本身占用的内存大小,32位系统是4字节,64位系统是8个字节; 对引用进行sizeof操作得到的是被绑定到的变量占用的内存大小。
- 指针可以有二级、三级等多级指针,引用没有。
1.函数传引用参数传递的是什么?
函数传指针参数相信大家都很清楚。那么函数传引用参数,到底传递的是什么呢?来看一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| #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); }
|
这段代码的执行结果是:
1 2 3
| $ ./a.out 000000000061fe4c 000000000061fe48 000000000061fe4c 000000000061fe48
|
从结果可以看出来,在passByRefrence
函数中的a,b变量地址和main函数中定义的a,b变量地址是一样的,貌似传引用就是传递的变量地址。也许上面的结果不能让你信服,那么我们看一个更有说服力的例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| #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函数反编译的结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| // 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
函数的传引用版本修改一下:
1 2 3 4 5
| int add(int &a, int &b) { int &p = a; return p + b; }
|
重新反编译得到add
函数的反汇编实现,为了方便对比,我把上一个版本也放在这里 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| // 版本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