前言
引用和指针都能「间接访问另一个对象」,于是很多人把它们当成同一种东西的两种写法:int&就是不能为空的int*,*p就是r。这个理解在简单函数传参时勉强够用,但一旦涉及重新绑定、对象生命周期、const限定和重载决议,就会出错。
最核心的差别有两条。第一,引用必须在定义时绑定到一个已存在的对象,之后不能重新绑定;对引用赋值是给被引用的对象赋值,不是让引用改指向别处。第二,引用不是独立对象,标准不要求它有地址、不要求它占存储;sizeof一个引用得到的是被引用类型的大小,而sizeof指针得到的是指针本身的大小。
本文从语法、语义、const、函数参数和生命周期五个方面对比两者,并给出选择建议。示例以 C++17 为标准,不涉及编译器实现细节的断言。
一、绑定与重新绑定
// 适用于 C++17
#include <iostream>
int main() {
int a = 10;
int b = 20;
int& ref = a; // ref 绑定到 a,且只能绑定这一次
ref = b; // 把 b 的值赋给 a,a 变成 20;ref 仍绑定 a
int* ptr = &a; // ptr 指向 a
ptr = &b; // ptr 改为指向 b,a 不受影响
std::cout << a << ' ' << b << ' ' << *ptr << '\n';
return 0;
}
按标准语义,a变成 20,b仍是 20,*ptr是 20。更关键的是地址:&ref与&a相同,且在执行ref = b之后仍然相同;ptr的值则从&a变成了&b。引用没有「改指向」这个操作,这是它与指针最本质的区别。
引用在声明时必须初始化,int& r;无法通过编译。指针可以先声明后赋值,也可以是空指针。引用不存在合法的「空引用」,即使通过解引用空指针来初始化引用,也是未定义行为(undefined behavior, UB):
// 适用于 C++17
int* p = nullptr;
int& r = *p; // UB:不能形成对空对象的引用
二、sizeof、地址与数组
sizeof(ref) | 被引用类型的大小,例如sizeof(int) |
sizeof(ptr) | 指针类型的大小,通常与平台字长相关 |
ptr + 1 | 指向下一个同类型元素(要求指向数组内) |
引用不是数组元素类型的好选择:不能定义引用的数组(int& arr[3]非法),但可以定义数组的引用(int (&arr)[3])。指针数组和指向数组的指针则是常见类型。函数参数写成int arr[]会退化为int*,而写成int (&arr)[3]能保留长度信息;这也是模板推导数组长度时常用的技巧。
对引用做++、--、+等运算,实际作用于被引用的对象;对指针做这些运算,改变的是指针值(或对指向对象做运算后再移动)。指针算术只允许在同一个数组(含尾后位置)内进行,越界解引用是 UB。
三、const 的四种组合
const与指针结合时,const在*左边修饰「被指对象」,在右边修饰「指针本身」:
const int* p | 指向 const int 的指针 | 能 | 不能 |
const int* const p | 两者都 const | 不能 | 不能 |
引用没有「引用本身 const」的说法。const int& r表示「不能通过 r 修改被引用对象」,被引用对象本身可能不是 const;它也可以绑定到临时对象,并延长该临时对象的生命周期到引用的作用域结束。这一点让const T&成为只读大对象参数的首选。
// 适用于 C++17
#include <string>
void print(const std::string& text); // 只读,不拷贝,可接临时对象
void modify(std::string& text); // 可修改,不能接临时对象
int main() {
print("temporary"); // 合法
// modify("temporary"); // 非法:非 const 左值引用不能绑定临时对象
std::string s = "hello";
modify(s); // 合法
return 0;
}
四、函数接口与多态
传引用和传指针都能避免拷贝并允许函数修改实参,但语义不同:
选择时看语义:参数一定存在且需要修改,用T&;参数可能缺失,用T*或std::optional<T>;只读大对象,用const T&;需要转移所有权,用智能指针或按值移动。
返回值方面,返回引用可以避免拷贝,但绝不能返回局部变量的引用。局部变量在函数返回时销毁,引用立即悬垂。返回指针同理。可以安全返回的引用包括:静态存储期对象、调用方传入的对象、成员变量(前提是对象生命周期覆盖使用期)。
多态与重载
通过基类指针或引用调用虚函数都能实现动态绑定;按值传递会发生对象切片。引用在语法上更接近值语义,指针则更适合表达「可能为空」或「需要重新指向」。
重载决议也会区分引用和指针:void f(int&)与void f(int*)是两个不同重载,f(x)和f(&x)分别匹配。const T&与T&也能构成重载,但传入非 const 左值时优先匹配T&,传入 const 对象或临时对象时只能匹配const T&。
常见坑点
- ❌ 返回局部变量的引用。
// 适用于 C++17
int& bad() {
int x = 1;
return x; // 返回后 x 已销毁,悬垂引用
}
✅ 按值返回,或返回生命周期足够长的对象。
// 适用于 C++17
int good() { return 1; }
- ❌ 认为
ref = other会让引用改绑。
// 适用于 C++17
int a = 1, b = 2;
int& r = a;
r = b; // 改的是 a 的值,r 仍绑定 a
✅ 需要改指向就用指针。
// 适用于 C++17
int* p = &a;
p = &b;
- ❌ 混淆
const int*与int* const。
// 适用于 C++17
const int* p = &a;
*p = 3; // 编译错误:被指对象是 const
p = &b; // 合法:指针本身可改
✅ 按「const 在 * 左边修饰对象,右边修饰指针」记忆。
// 适用于 C++17
int* const q = &a; // q 不能改指向,但可以 *q = 3
- ❌ 用空指针初始化引用。
// 适用于 C++17
int* p = nullptr;
int& r = *p; // UB
✅ 引用必须绑定有效对象;可空语义用指针表达。
// 适用于 C++17
int* p = nullptr;
if (p != nullptr) { int& r = *p; /* ... */ }
- ❌ 对非数组指针做算术。
// 适用于 C++17
int x = 1;
int* p = &x;
++p; // 指向不存在的对象,解引用即 UB
✅ 只在数组范围内做指针算术。
// 适用于 C++17
int arr[3] = {1, 2, 3};
int* p = arr;
++p; // 指向 arr[1]
- ❌ 让引用成员阻止默认赋值。
// 适用于 C++17
struct S {
int& ref;
};
S a{x}, b{y};
a = b; // 引用成员使默认赋值被删除
✅ 需要可赋值就不要用引用成员,改用指针或值成员。
// 适用于 C++17
struct S { int* ptr; };
- ❌ 返回
c_str()后继续修改字符串。
// 适用于 C++17
const char* p = s.c_str();
s += "more"; // 可能导致缓冲区重新分配
std::cout << p; // p 可能悬垂
✅ 需要长期保存时复制到std::string。
// 适用于 C++17
std::string copy = s;
- ❌ 认为引用一定占内存、一定和指针一样大。
// 适用于 C++17
static_assert(sizeof(int&) == sizeof(int*)); // 不成立的假设
✅ 只依赖标准语义,不依赖引用的布局;sizeof(ref)按被引用类型计算。
总结
| 常见用途 | 参数、返回值、运算符重载 | 可选参数、动态对象、数组遍历 |
引用和指针不是替代关系,而是表达不同意图的工具:引用强调「就是它,不能为空,不会换」,指针强调「指向某个位置,可以为空,可以换」。把意图写进类型里,接口会比注释更可靠。