声明引用的写法是 类型 &引用名 = 变量;,比如 int &r = a;。三条铁律必须背下来:① 声明时必须初始化(int &r; 直接编译不过);② 绑定后终身不能改绑(r = b; 不是"改指 b",而是"把 b 的值赋给 a");③ 不能为空(没有"空引用"这种东西)。注意这里的 & 是第三种身份——不是取地址。
7.8 讲了"引用就是别名"。这一节把规则钉死。其中"不能改绑"这条最容易踩,因为它和不改绑的写法长得一模一样——r = b; 看起来很像"让 r 指向 b",实际却是"赋值"。搞清这一条,7.8.2 的引用传参才不会用错。另外,本章你已经见过 & 的另外两种身份(取地址、按位与),这里补上第三种。
一、声明与使用
int a = 10;
int &r = a; // r 是 a 的引用(别名)
读法:int &r = a; = "r 是 a 的另一个名字"。
用起来和普通变量一模一样:
r = 20; // 等价于 a = 20
cout << r; // 20
cout << &r; // 和 &a 完全相同
不需要任何 *——这是引用比指针好用的地方。
二、三条铁律
铁律 1:声明时必须初始化
int a = 10;
int &r = a; // ✅ 一开始就绑定好
int &r2; // ❌ 编译错误:引用必须初始化
为什么? 因为引用是"别名",一个没主的别名根本不存在——总得先有个人,才能给他起外号。
铁律 2:绑定后终身不能改绑(最容易踩)
int a = 10, b = 20;
int &r = a; // r 绑定了 a
r = b; // ⚠️ 这不是"让 r 改绑 b"!
// 而是"把 b 的值赋给 r(也就是 a)"
cout << a; // 20 ← a 变了
cout << b; // 20
cout << &r; // 仍然等于 &a(没改绑)
这一条最反直觉:指针可以 p = &b;(改指),引用没有这样的语法。任何出现在 r 左边的赋值,都是"给 a 赋值"。
记忆法:外号不能换人。你不能通过"喊老王"来指另一个王小明。
铁律 3:不能为空
int &r = NULL; // ❌ 没有意义:引用必须绑一个真实存在的对象
指针可以是"空指针"(7.4.4),引用不行——这也是引用更安全的原因:用引用时,永远不用担心"它是不是空的"。
三、& 的四种身份(补全)
同一个 &,在不同位置干完全不同的事:
| 出现位置 | 身份 | 例子 | 出处 |
|---|---|---|---|
| 声明里的类型后面 | 引用声明 | int &r = a; |
本节 |
| 表达式里、变量前面 | 取地址 | p = &a; |
7.4.2 |
| 两个整数中间 | 按位与 | 5 & 3 |
3.1.5 |
| 两个条件中间(写两个) | 逻辑与 | a && b |
3.1.3 |
怎么认:
- 出现在声明语句里(前面有类型)→ 引用;
- 出现在表达式里(前面是个变量)→ 取地址;
- 左右都是值、只有一个 & → 按位与;
- 两个 & → 逻辑与。
四、引用 vs 指针:一张表说清
| 对比项 | 指针 | 引用 |
|---|---|---|
| 是不是变量 | ✅ 是(自己占空间) | ❌ 不是(只是别名) |
| 能不能为空 | ✅ 能(nullptr) |
❌ 不能 |
| 能不能改指/改绑 | ✅ 能(p = &b;) |
❌ 不能 |
要不要 * |
✅ 取值要 *p |
❌ 直接用 |
要不要 & |
✅ 传参要 &x |
❌ 直接用 |
| 能不能做指针运算 | ✅ p + 1 |
❌ 不能 |
| 安全性 | 较低(野指针、空指针) | 较高(不会空、不会跑偏) |
| 主要用途 | 数组遍历、动态内存、可空语义 | 函数参数(7.8.2)、运算符重载 |
一句话总结:引用是"限权版的指针"——它砍掉了所有容易出错的自由度(改指、为空、运算),换来的是安全和简洁。
五、动手 Demo:亲手验证"不能改绑"
#include <iostream>
using namespace std;
int main() {
int a = 10, b = 20;
int &r = a; // r 绑定 a
cout << "初始: a=" << a << " b=" << b << " r=" << r << endl; // 10 20 10
r = b; // ⚠️ 看似"改绑",其实是 a = b
cout << "r=b 后:a=" << a << " b=" << b << " r=" << r << endl; // 20 20 20
cout << "---- 地址验证 ----" << endl;
cout << "&a = " << &a << endl;
cout << "&r = " << &r << endl; // 与 &a 相同(没改绑)
cout << "&b = " << &b << endl; // 另一个地址
int &r2 = b; // 想要"指向 b",只能另起一个引用
cout << "r2 绑定 b:&r2 = " << &r2 << endl; // 与 &b 相同
return 0;
}
看到什么算成功:
初始: a=10 b=20 r=10
r=b 后:a=20 b=20 r=20 ← a 被改了!
---- 地址验证 ----
&a = 0x61ff04
&r = 0x61ff04 ← 和 &a 一样:r 还是绑着 a
&b = 0x61ff00
r2 绑定 b:&r2 = 0x61ff00 ← 另起的 r2 才绑上了 b
最关键的对照:r = b; 之后 a 变成了 20(说明是赋值),而 &r 依然等于 &a(说明没改绑)。想"改指",只能另声明一个引用(int &r2 = b;)——这是引用和指针最重要的行为差异。
30 秒实验:把 int &r = a; 改成 int &r;(去掉初始化),重新编译——报错 declared as reference but not initialized。再把它改成 int &r = NULL;——同样报错。两条铁律一次试完。
六、常见误区
- "
r = b;能让 r 改绑到 b"——不能。它是"把 b 的值赋给 a"。这就是最需要动手验证的一条。 - "引用声明里的
&是取地址"——不是,那是引用声明符(本节第三节的四种身份表)。 - "引用和指针可以混着用"——语法上天差地别。引用不能做
p + 1、不能p = &x、不能判空。 - "引用必须在定义时就用到,很受限"——恰恰相反,正因为它"绑定即终身",编译器能做出更好的优化,也更不容易出错。
- "引用比指针慢"——不会。绝大多数情况下,编译器生成的是同样的代码。选哪个看语义:需要"可空、可改指"用指针;"只是另一个名字"用引用。
自测三问(点开对答案)
1. 引用的三条铁律?——必须初始化、绑定后不能改绑、不能为空。 2. `int a=1, b=2; int &r = a; r = b;` 之后 `a` 是几?——`2`。`r = b` 是给 `a` 赋值,不是改绑。 3. 想要一个"能改指"的别名机制,用什么?——**指针**(`p = &b;`)。引用做不到。- ☐ 7.8.2 使用引用传递参数(下一节)
相关笔记
- 引用 —— 引用是什么
- 使用引用传递参数 —— 下一节:最漂亮的 swap
- 指针运算符和取地址运算符 ——
&的取地址身份 - 位运算符 ——
&的按位与身份 - 🔷 C++基础 · 索引 —— 全课路线图