引用是已有对象的别名,指针是指向地址的变量;引用必须初始化、不可重绑定、不为空、不占存储,而指针可延迟赋值、可空、可算术、占固定字节。

引用不是“安全的指针”,它和指针是两种语义完全不同的机制:一个是指向地址的变量,一个是已有对象的别名。选错会导致编译失败、悬空访问或行为不符合预期。
引用必须初始化,指针可以延迟赋值
这是最常触发编译错误的差异点。声明 int& r; 直接报错,而 int* p; 合法(尽管危险)。引用绑定是“一次性契约”——int a = 10; int& r = a; 之后,r 就永远代表 a,不能通过任何语法让它指向 b。
指针则自由得多:int* p = nullptr;、p = &a;、p = &b; 全部合法。但这也意味着你必须自己保证每次解引用前 p 不为空、不悬空。
- 函数参数用引用时,调用方传入的实参必须是有效对象,编译器强制检查
- 指针参数(如
void f(int* p))允许传nullptr,但你要在函数体内写if (p == nullptr) return; - 类成员中声明引用(
int& ref;)必须在构造函数初始化列表里绑定,否则编译不过
引用不能为 null,指针可以是 nullptr
没有“空引用”这回事。int& r = *p; 看似取指针所指,但如果 p 是 nullptr,这行代码直接触发未定义行为(多半崩溃),编译器不会拦你——因为 *p 本身已非法。
立即学习“C++免费学习笔记(深入)”;
而 int* p = nullptr; 是明确定义的,且可安全比较:if (p != nullptr) { *p = 42; }。
- 接口设计上,若参数“必须提供有效对象”,优先用引用(
void process(const std::string& s)) - 若参数“可选”,比如日志回调可能为空,必须用指针(
void set_callback(void (*cb)(int))) -
std::optional<t></t>出现后,部分原本靠指针表达“可选”的场景,现在更倾向用std::optional<const t></const>(注意:不能有optional<t></t>)
引用不占存储,指针占固定字节
sizeof(int&) 返回的是 sizeof(int),不是指针大小;sizeof(int*) 在 64 位系统上稳定返回 8。这不是“优化技巧”,而是语言规范:引用不是对象,没有地址(&r 实际取的是 a 的地址)。
这也导致一些限制无法绕过:
- 不能有引用数组:
int& arr[10];❌ 编译失败(每个元素都需独立绑定,无法声明) - 不能有指向引用的指针:
int&* ptr;❌ 语法错误(引用本身不可取地址作为目标) - 不能有引用的引用:
int&&& r3;❌ 但右值引用int&&是特例,和这里无关 - 指针可以做算术:
int arr[3] = {1,2,3}; int* p = arr; p++;→ 指向arr[1];而int& r = arr[0]; r++;只是把arr[0]加 1
函数参数和返回值中的典型误用
传参时用引用本意是避免拷贝,但若你不小心返回了局部变量的引用,就制造了悬垂引用:const std::string& get_name() { std::string tmp = "alice"; return tmp; } ——tmp 析构后,返回的引用指向垃圾内存。
相比之下,返回指针虽也危险,但至少 return &tmp; 这种写法更刺眼,且现代静态分析工具(如 clang-tidy)对悬垂引用的检测仍弱于悬垂指针。
- 返回局部对象?用值语义(
std::string)或智能指针(std::unique_ptr<t></t>),别用引用 - 想让函数“输出多个结果”?用引用参数(
void parse(const std::string& s, int& code, std::string& msg)),别用指针——除非某个输出项确实可为空 - 重载操作符(如
operator[])常返回引用以便链式赋值:vec[0] = 42;;这里返回指针会迫使用户写*vec[0] = 42;,破坏直觉
真正难的不是记住“引用不能改绑”或“指针可为空”,而是在复杂对象生命周期、模板推导、移动语义交织的场景下,判断哪个语义更贴近你的真实意图。比如 auto&& 是万能引用,T& 和 T&& 在模板中会折叠,这些都不是单纯靠“指针 vs 引用”二分能覆盖的——但所有高级用法,都建立在彻底吃透这两者底层不可互换的前提之上。


















