学习过C语言的小伙伴会同时接触这两种概念,指针&引用。其定义为:指针是所指内存的地址,引用是别名,引用必须初始化。
接下来我们站在汇编的角度来分析指针和引用。
#include<stdio.h>
int main(){
char *p; //声明一个指针p
char a = 'a';
p = &a; // 引用a的值赋值给p
return 0;
}
上面C语言对应的汇编程序:
0x0000000000401531 <+1>: mov rbp,rsp ;栈顶栈底指向同一位置
0x0000000000401534 <+4>: sub rsp,0x30 ;分配栈空间,向下分配
0x0000000000401538 <+8>: call 0x4020e0 <__main> ;调用main函数
0x000000000040153d <+13>: mov BYTE PTR [rbp-0x9],0x61 ; 初始化一个字节空间,起始地址为rbp-0x9,a对应的ASCII码为0x61
0x0000000000401541 <+17>: lea rax,[rbp-0x9] ;将rbp-0x9的对应的地址值赋值给rax。此时rbp-0x9的对应的地址值也就是变量a在栈中对应的地址。可以将rbp-0x9看做&a。
0x0000000000401545 <+21>: mov QWORD PTR [rbp-0x8],rax ;为指针分配空间,该空间内的值为rax记录的地址值。可以将rbp-0x8看做p。
0x0000000000401549 <+25>: mov eax,0x0 ;准备return
0x000000000040154e <+30>: add rsp,0x30
0x0000000000401552 <+34>: pop rbp
0x0000000000401553 <+35>: ret
从上述代码可以看到,在汇编语言角度,引用和指针没有区别,可以使用相同的运算符(地址运算符)来表示。
But 在C++层面:
- 引用区别于指针的特性都是编译器约束完成的,在汇编层面引用和指针是相同的,都是内存地址。
- 指针是实实在在的变量,有自己的内存存储空间(可以存储在数据段中),它可以指向任何有效的变量;引用变量本身没有自己的实际存储空间(可以存储在代码段中),操作引用变量,就是在操作实际变量。
- 指针可以不初始化,通过赋值可以指向任意同类型的内存;但是引用必须初始化,而且引用一旦引用一块内存,再也不能引用其它内存了,即引用不能被改变。
关于1比较容易理解,编译器层面遇到指针和引用有不同的处理机制。
但是关于2和3,我们使用代码来看一下:
// 源码
#include <stdio.h>
int main(){
int a = 1; // 声明并初始化变量a
int& ref = a; // 声明并初始化引用ref,现在ref引用的是a
int b = 2;
ref = b;
int* e = &ref;
int* d = &ref;
printf("%d\n", a);
printf("%d\n", b);
printf("%d\n", *e);
printf("%d", *d);
return 1;
}
# 上面源码对应的汇编指令
mov DWORD PTR [rbp-0x24],0x1:这行代码将立即数值1(即变量a的初始值)存储到基址指针(rbp)减去36(0x24)的内存位置。这对应于源代码中的 int a = 1;。
lea rax,[rbp-0x24]:这行代码将rbp减去36的地址加载到rax寄存器中。这是在为引用 ref 获取变量 a 的地址。
mov QWORD PTR [rbp-0x8],rax:这行代码将rax寄存器的值(即变量a的地址)存储到rbp减去8的内存位置。这对应于源代码中的 int& ref = a;。
mov DWORD PTR [rbp-0xc],0x2:这行代码将立即数值2(即变量b的初始值)存储到rbp减去12(0xc)的内存位置。这对应于源代码中的 int b = 2;。
mov rax,QWORD PTR [rbp-0x8]:这行代码将rbp减去8的内存位置的值(即变量a的地址)加载到rax寄存器中。这是在获取引用 ref 所引用的对象的地址。
mov edx,DWORD PTR [rbp-0xc]:这行代码将rbp减去12的内存位置的值(即变量b的值)加载到edx寄存器中。
mov DWORD PTR [rax],edx:这行代码将edx寄存器的值(即变量b的值)存储到rax寄存器指向的内存位置(即变量a的地址)。这对应于源代码中的 ref = b;。
mov rax,QWORD PTR [rbp-0x8]:这行代码将rbp减去8的内存位置的值(即变量a的地址)加载到rax寄存器中。这是在获取引用 ref 所引用的对象的地址。
mov QWORD PTR [rbp-0x18],rax:这行代码将rax寄存器的值(即变量a的地址)存储到rbp减去24(0x18)的内存位置。这对应于源代码中的 int* e = &ref;。
mov rax,QWORD PTR [rbp-0x8]:这行代码将rbp减去8的内存位置的值(即变量a的地址)加载到rax寄存器中。这是在获取引用 `ref` 所引用的对象的地址。
mov QWORD PTR [rbp-0x20],rax:这行代码将rax寄存器的值(即变量a的地址)存储到rbp减去32(0x20)的内存位置。这对应于源代码中的 int* d = &ref;。
mov eax,DWORD PTR [rbp-0x24]
mov edx,eax
lea rcx,[rip+0x2a88] # 0x404000
call 0x402b68 <printf>
mov eax,DWORD PTR [rbp-0xc]
mov edx,eax
lea rcx,[rip+0x2a77] # 0x404000
call 0x402b68 <printf>
mov rax,QWORD PTR [rbp-0x18]
mov eax,DWORD PTR [rax]
mov edx,eax
lea rcx,[rip+0x2a63] # 0x404000
call 0x402b68 <printf>
mov rax,QWORD PTR [rbp-0x20]
mov eax,DWORD PTR [rax]
mov edx,eax
lea rcx,[rip+0x2a53] # 0x404004
call 0x402b68 <printf>
mov eax,0x1
add rsp,0x50
pop rbp
ret
解释2和3:
第10行指令中,rbp-24 对应的是 e 的内存位置。
第10行指令中,rbp-32 对应的是 d 的内存位置。
观察前面的指令发现:
ref并没有自己的内存空间。rbp-8 记录的是运算过程需要的内存中某个位置的值,并不代表是ref的值。即使上面的代码中不存在ref引用,使 int* e = &a,也需要这样一个 rbp-8 记录变量 a 对应的值的内存位置。So Ref的存在并没有使栈的深度增加。即没有实际的储存空间。
关于引用一旦引用一块内存,再也不能引用其它内存了。这是因为一旦引用被编译为内存位置之后,接下来所有对引用的操作就是在操作该内存位置对应的值,无法再修改该引用对应的内存位置。
引用的历史渊源:
本来之父也和你一样想的,有指针就够了,一样能实现相应的功能,没必要再多一个语法设施。但后来为了加运算符重载,没有引用的话,前自增的语义就难以说明清楚,这是引入引用概念的历史背景。后来你可以发现,线性容器所重载的下标运算符,迭代器和智能指针所重载的间接访问运算符,输入流和输出流的链式调用,这一切都是离不开引用语义的。 抛开运算符重载的历史因素,引用在大多数场合下完全可以视作指针的语法糖——在底层的汇编的层面讲他们是一样实现的。不过有了引用以后,确实写代码可以方便很多。比如最常见的手法就是利用引用免掉一阶的指针。在 C 里面,你要在函数中修改一个一级指针,形参里得声明成二级指针,但在 C++ 里形参声明成一级指针的引用就可以了。别看只是降了一阶,但人类的思维理解高维的概念很困难,问题降一阶以后考虑问题就可以轻松很多。 有了引用以后,代码也变得简练。以前在 C 里,如果要设计一个函数处理大对象,则不得不以指针做参数,那就不得不声明临时变量去存储中间结果。 Matrix a, b, c; getMatrixA(&a); getMatrixB(&b); addMatrix(&a, &b, &c); printMatrix(&c); 而在 C++ 里,则完全不需要考虑这些,完全可以以引用当参数,代码又清晰,又不用担心会拷贝而带来效率瓶颈。 print(addMatrix(getMatrixA(), getMatrixB()));
在Java层面:
可以参考本文的回答:java的引用明明和指针没什么本质区别,java为什么还宣称没有指针并把这个当作语言的优点?