C++中的指针和引用是两种常用的间接访问变量的方式,二者在使用上有很多相似之处,但底层实现逻辑存在本质差异。理解这些差异不仅有助于写出更规范的代码,也能在面试中清晰回答相关考察点。

一、指针与引用的表层区别
从语法层面来看,指针和引用存在以下明显差异:
- 指针是一个变量,存储的是另一个变量的内存地址;引用是某个变量的别名,和原变量指向同一块内存空间。
- 指针可以为空,初始化后可以修改指向的对象;引用必须在定义时初始化,且一旦初始化后不能改变绑定的对象。
- 指针可以有多级,比如二级指针
int**;引用只有一级,不存在多级引用。 - 指针的大小取决于系统位数,32位系统占4字节,64位系统占8字节;引用的大小和绑定的原变量大小一致。
- 使用指针访问指向的对象需要用解引用符
*,引用可以直接使用,和原变量的使用方式完全相同。
二、从底层实现角度分析差异
从编译器的实现逻辑来看,引用本质上是通过指针实现的,但编译器对引用做了更多的限制,保证其使用的安全性和规范性。
我们可以写一段简单的测试代码,观察二者的内存布局:
#include <iostream>
using namespace std;
int main() {
int a = 10;
int* p = &a; // 指针p存储a的地址
int& r = a; // 引用r是a的别名
cout << "a的地址: " << &a << endl;
cout << "p存储的地址: " << p << endl;
cout << "r的地址: " << &r << endl; // 引用r的地址和a的地址相同
cout << "p自身的地址: " << &p << endl; // 指针p自身有独立的内存空间
return 0;
}
上述代码的输出结果中,a的地址、r的地址、p存储的地址三者是相同的,而p自身的地址是另一个独立的内存地址,这说明指针本身占用独立的内存空间,而引用和原变量共享同一块内存空间。
三、从汇编角度对比二者实现
我们可以通过查看上述代码的汇编输出,更直观地看到指针和引用的底层执行逻辑。以x86-64架构下的GCC编译结果为例,核心汇编代码如下:
main:
push rbp
mov rbp,rsp
sub rsp,0x20 ; 分配栈空间
mov DWORD PTR [rbp-0x14],0xa ; 将10存入a的内存空间(rbp-0x14是a的地址)
lea rax,[rbp-0x14] ; 将a的地址加载到rax寄存器
mov QWORD PTR [rbp-0x8],rax ; 将rax的值(a的地址)存入指针p的内存空间(rbp-0x8是p的地址)
; 引用r的初始化没有生成额外的汇编指令,编译器直接将r映射到a的地址[rbp-0x14]
; 访问指针p指向的值
mov rax,QWORD PTR [rbp-0x8] ; 从p的内存空间取出a的地址
mov eax,DWORD PTR [rax] ; 通过地址访问a的值
; 访问引用r的值
mov eax,DWORD PTR [rbp-0x14] ; 直接通过a的地址访问值,和访问a的汇编指令完全一致
leave
ret
从汇编代码可以看出,指针的操作需要两步:先从指针的内存空间取出存储的地址,再通过该地址访问目标值;而引用的操作直接通过原变量的地址访问值,和直接使用原变量的汇编指令完全相同,这也验证了引用是原变量的别名这一特性。
四、常见面试考察点总结
结合底层实现和汇编分析,面试中回答二者的区别可以按照以下逻辑组织:
- 先说明语法层面的差异:是否可为空、是否可修改指向、是否有自身内存空间等。
- 再说明底层实现:引用本质是指针的封装,编译器限制了引用的修改操作,保证其和原变量绑定。
- 最后结合汇编说明执行逻辑:指针需要额外的寻址操作,引用直接访问原变量内存,执行效率更高。
需要注意的是,虽然引用的底层实现和指针相关,但二者在C++语法层面是完全不同的概念,不能将引用等同于指针使用,比如不能定义引用的引用,不能定义元素为引用的数组等。