图 1:本文封面,指针存地址、引用是别名的内存形态对比。
先说明属性:这是一篇经典题解析,题目出自本地历史面试资料整理,与任何公司的当前招聘无关。引用与指针的区别是 C++ 面试的常驻题,背出「引用是别名」只能算开场白,面试官想听的是语义、内存形态、使用约束三个层面各自的差异,以及函数传参时怎么选。
一句话区分:指针是存地址的变量,引用是对象的别名。
指针本身是一个对象:它占用自己的内存(64 位平台固定 8 个字节),里面存的是另一个对象的地址,通过解引用间接访问目标。引用则不是对象,它是给已有对象起的第二个名字——语义上没有独立存储,对引用的任何操作就是对原对象本身的操作。这个本质差异决定了后面所有具体区别。
六个维度的逐项对照。
初始化:指针可以先声明后赋值,甚至不初始化(虽然危险);引用在定义时必须绑定到一个有效对象,不存在「先声明再绑」的写法。可空性:指针可以为 nullptr,这是它表达「没有」的合法方式;引用不可为空,绑定之后始终指向有效对象——正因如此,用引用做参数的函数不需要判空。改指向:指针随时可以改指别的对象;引用终身绑定,初始化后无法再改绑,对引用赋值改的是被绑定对象的值。sizeof:对指针取 sizeof 得到指针本身的大小;对引用取 sizeof 得到的是被引用对象的大小,这直接印证「引用就是对象本身」。多级间接:指针可以套娃,int 的二级指针、三级指针都合法;不存在引用的引用、引用的数组、指向引用的指针,语言层面直接禁止。运算能力:指针可以做加减、遍历数组;引用没有任何地址运算。
用代码实测一遍。
下面这段程序把上面的论断逐条落实(Clang 17,g++ -std=c++17 ref.cpp && ./a.out):
#include <iostream>
using namespace std;
void addOne(int& r) { r++; } // 传引用:直接改原对象
void addOnePtr(int* p) { if (p) (*p)++; } // 传指针:要判空
int main() {
int x = 42;
int* p = &x;
int& r = x;
cout << "sizeof(p) = " << sizeof(p) << '\n';
cout << "sizeof(r) = " << sizeof(r) << '\n';
cout << "r = " << r << ", r 与 x " << ((&r == &x) ? "同一块内存" : "不同") << '\n';
r = 100; // 通过引用改的是 x 本身
cout << "r=100 之后 x = " << x << '\n';
int y = 7;
p = &y; // 指针可以改指向
cout << "p 改指 y 后 *p = " << *p << '\n';
// r = y; // 注意:这不是改绑定,是把 y 的值赋给 x!
int arr[3] = {1, 2, 3};
int* q = arr;
cout << "指针算术 *(q+1) = " << *(q + 1) << '\n';
int m = 10;
addOne(m);
cout << "addOne(m) 后 m = " << m << '\n';
addOnePtr(nullptr); // 指针可以为空,语义上表达「没有」
cout << "传 nullptr 合法,函数内判空跳过" << '\n';
return 0;
}
实测输出:
sizeof(p) = 8
sizeof(r) = 4
r = 42, r 与 x 同一块内存
r=100 之后 x = 100
p 改指 y 后 *p = 7
指针算术 *(q+1) = 2
addOne(m) 后 m = 11
传 nullptr 合法,函数内判空跳过
两个最容易被追问的点都在里面:其一,sizeof(r) 等于 4 而不是 8,因为引用没有自己的尺寸;其二,r = y 这样的语句不是「引用改绑」,而是把 y 的值赋给 x——引用一旦绑定,名字和对象的对应关系终身不变。
图 2:六维对照表、传参选择规则与底层一注。
函数传参怎么选,以及一个底层注脚。
三条经验规则:对象必然存在、且不转移所有权,传引用(只读就传常量引用),接口干净又安全;参数允许缺省,用指针,nullptr 就是「没有」的显式表达;需要在函数里修改调用方的指针本身(比如输出一个新建的对象头),用指针的指针或指针的引用。最后补一个底层视角:大多数编译器把引用实现为指针,生成的机器码常常一模一样,但「别名」的语义约束——必须初始化、不可为空、不可改绑——是编译器在语言层面强制的,这才是引用比指针安全的来源。也要记住安全不是绝对的:返回局部变量的引用照样是悬空的未定义行为,引用救不了生命周期错误。答题时按「本质—约束—传参选择—底层实现」四层递进,比干背区别清单高一个档次。你在项目里定传参规范时,引用和指针的边界是怎么划的?欢迎分享。