指针可为空,引用必须绑定有效对象;指针是存储地址的变量,引用是对象别名且声明时必须初始化,不可重绑定,无独立内存空间,不支持算术运算,sizeof引用等于所引类型大小。

指针可以为空,引用必须绑定有效对象
这是最常被忽略的底层差异:指针变量本身存储的是一个地址值,nullptr 是合法值;而引用在 C++ 标准中被定义为“对象的别名”,编译器要求它在声明时就必须用一个已存在的、可取地址的对象初始化,且之后不能重绑定。
常见错误现象:int& r = *static_cast 看似只是解引用空指针,实际会触发未定义行为(UB),不是“引用为空”,而是根本没成功构造出引用——这行代码在大多数编译器下会直接崩溃或产生不可预测结果。
实操建议:
- 函数参数用
const int&接收只读大对象(避免拷贝),但前提是调用方传入的是左值或能绑定的右值(如临时对象) - 若参数可能“不存在”,必须用
const int*或std::optional,不能用引用加判空逻辑(引用无法判空) - 类成员中几乎从不声明引用类型(除非是构造时强制绑定且生命周期严格可控),因为没有默认构造/赋值语义
指针有独立内存地址,引用通常不占存储空间
指针变量本身是一个对象,占用 4 或 8 字节(取决于平台),有自己的地址(&p 有意义);而引用在绝大多数编译器实现中是符号级别的别名,不分配额外栈空间——&r 返回的是它所引用对象的地址,不是“引用自身的地址”。
立即学习“C++免费学习笔记(深入)”;
但注意:这不是语言标准保证。标准只要求引用的行为等价于别名,不规定是否占空间。某些特殊场景(如作为结构体成员、对引用取地址再转成指针)可能迫使编译器为其分配空间(例如 struct S { int& r; }; 在 GCC 中 sizeof(S) 可能为 8),但这属于实现细节,不应依赖。
性能影响:
- 局部引用几乎零开销,编译器通常直接优化掉符号层,生成和直接操作原变量一样的汇编
- 指针多一次间接寻址(
*p),但现代 CPU 的缓存和预测机制下,差别微乎其微 - 真正影响性能的是数据局部性,而不是“用指针还是引用”这一层选择
指针支持算术运算和多级间接,引用只能单层绑定
int* p = &a; p++; 合法,int& r = a; r++; 是对 a 自增,但 r++ 本身不能改变绑定目标。引用没有“++”、“+=”这类重定向操作符。
这意味着:
- 数组遍历、内存块操作必须用指针(
int* iter = arr; while (iter != end) { ... ++iter; }) - 动态多态调用(
Base* p = new Derived; p->func();)依赖指针/智能指针,引用虽可绑定派生类对象,但无法在运行时切换目标 - 函数返回引用(
int& get() { return data; })是安全的,只要返回的是静态存储期或调用者生命周期更长的对象;返回局部变量的引用(int& bad() { int x=0; return x; })是典型悬垂引用,UB
面试常踩坑:把引用当成“自动解引用的指针”
很多候选人说“引用就是不能改指向的指针”,这种类比在理解上方便,但在底层机制和语言规则上完全错误。引用不是语法糖封装的指针,它不持有地址、不参与地址计算、不支持 reinterpret_cast 转换为其他类型指针(而 int* 可以强转为 char* 做字节操作)。
容易被忽略的关键点:
-
sizeof(int&)永远等于sizeof(int),不是指针大小 - 引用类型无法做模板偏特化区分(
template对引用无效)struct is_ptr : std::false_type {}; template struct is_ptr : std::true_type {}; - 调试器里看不到“引用变量”的独立内存位置,Watch 表达式中
&r显示的是目标地址,不是引用自身地址(因为它通常不存在)
真正要深挖底层,得看汇编输出:g++ -S -O2 下对比 void f(int& r) { r = 42; } 和 void f(int* p) { *p = 42; } —— 你会发现它们生成的指令往往一模一样。区别不在运行时,而在编译期约束和语义表达力。










