🔷 第七章 数组、指针和引用
📄 一句话概括

声明引用的写法是 类型 &引用名 = 变量;,比如 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;——同样报错。两条铁律一次试完。

六、常见误区

  1. "r = b; 能让 r 改绑到 b"——不能。它是"把 b 的值赋给 a"。这就是最需要动手验证的一条。
  2. "引用声明里的 & 是取地址"——不是,那是引用声明符(本节第三节的四种身份表)。
  3. "引用和指针可以混着用"——语法上天差地别。引用不能做 p + 1、不能 p = &x、不能判空。
  4. "引用必须在定义时就用到,很受限"——恰恰相反,正因为它"绑定即终身",编译器能做出更好的优化,也更不容易出错。
  5. "引用比指针慢"——不会。绝大多数情况下,编译器生成的是同样的代码。选哪个看语义:需要"可空、可改指"用指针;"只是另一个名字"用引用。
自测三问(点开对答案) 1. 引用的三条铁律?——必须初始化、绑定后不能改绑、不能为空。 2. `int a=1, b=2; int &r = a; r = b;` 之后 `a` 是几?——`2`。`r = b` 是给 `a` 赋值,不是改绑。 3. 想要一个"能改指"的别名机制,用什么?——**指针**(`p = &b;`)。引用做不到。

相关笔记