疫情在家,彻底弄清指针和引用的区别

54 阅读4分钟

学习过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++层面:

  • 引用区别于指针的特性都是编译器约束完成的,在汇编层面引用和指针是相同的,都是内存地址。
  • 指针是实实在在的变量,有自己的内存存储空间(可以存储在数据段中),它可以指向任何有效的变量;引用变量本身没有自己的实际存储空间(可以存储在代码段中),操作引用变量,就是在操作实际变量。
  • 指针可以不初始化,通过赋值可以指向任意同类型的内存;但是引用必须初始化,而且引用一旦引用一块内存,再也不能引用其它内存了,即引用不能被改变。

历史渊源:

本来之父也和你一样想的,有指针就够了,一样能实现相应的功能,没必要再多一个语法设施。但后来为了加运算符重载,没有引用的话,前自增的语义就难以说明清楚,这是引入引用概念的历史背景。后来你可以发现,线性容器所重载的下标运算符,迭代器和智能指针所重载的间接访问运算符,输入流和输出流的链式调用,这一切都是离不开引用语义的。 抛开运算符重载的历史因素,引用在大多数场合下完全可以视作指针的语法糖——在底层的汇编的层面讲他们是一样实现的。不过有了引用以后,确实写代码可以方便很多。比如最常见的手法就是利用引用免掉一阶的指针。在 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为什么还宣称没有指针并把这个当作语言的优点?

参考文章