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

引用数组里的某一格,靠下标:数组名[下标]。这个写法既能读也能写(是 3.4 说的"左值")。它最大的坑是——越界不报错:写了 a[5] 而数组只有 5 格,编译器一声不吭,程序照跑,只是悄悄踩坏了别人的数据。

📝 本节和前面几节的关系

7.1.1 把柜子订好了,这一节学"怎么开柜门"。a[i] 里的 i 可以是常量,也可以是变量或表达式——配上 5.2 的 for,就得到"遍历"这个超级常用的套路。另外,a[i] 能当左值直接赋值(a[0] = 5;),是因为它指向内存里一个真实存在的格子(3.4 左值定义为"有地址"的盒子,数组格子天然满足)。

一、引用的写法

数组名[下标]
int a[5] = {10, 20, 30, 40, 50};

a[0]        // 10 —— 第一格
a[2]        // 30 —— 第三格(不是第二格!)
a[4]        // 50 —— 最后一格

a[1] = 99;  // 也能赋值:改的是第二格

下标与格子的对应关系(务必记牢):

你想要 写什么 说明
第 1 格 a[0] 下标 0
第 i 格 a[i-1] 下标比序号小 1
最后一格 a[4] 长度为 5 时
不存在的格 a[5] ⚠️ 越界!

二、下标可以是"会变的"

[] 里不限于写死的数字,任何算出整数的表达式都行:

int a[5] = {10, 20, 30, 40, 50};
int i = 3;

cout << a[i] << endl;        // 40 —— 变量当下标
cout << a[i - 1] << endl;    // 30 —— 表达式当下标
cout << a[2 * 1] << endl;    // 30 —— 随便算

正因为下标能变,才能配 for 一次走遍整排柜子——这就是遍历:

for (int i = 0; i < 5; i++) {
    cout << a[i] << " ";     // i 依次取 0 1 2 3 4
}

为什么是 i < 5 而不是 i <= 5?因为 i <= 5 会让 i 取到 5,而 a[5] 越界。这个 < 与 <= 的一字之差,是数组相关 bug 的头号来源。

三、最大的坑:越界不报错

看这段代码,你觉得会发生什么?

int a[3] = {1, 2, 3};
int b = 100;

a[3] = 999;                // ⚠️ a 只有 3 格,下标 3 已经越界
cout << b << endl;         // 打印什么?

答案:很可能打印不出 100。原因在于数组相邻存放(7.1 的内存图):a 后面的内存是谁,编译器不管你。而 a[3] 位置的那块内存,可能正好就是变量 b 的位置。于是 a[3] = 999 悄悄把 b 改成了 999。

为什么编译器不拦?因为 a[3] 在编译器眼里只是"从头往后挪 3 格",语法完全合法——它没有义务去数你订了几格。这就是 C/C++ 有名的"信任程序员"哲学:速度快、灵活,代价是自己看住下标。

安全尾巴:后面的章节会介绍更安全的容器(vector、string),它们会替你把关下标。但理解数组越界,是理解"内存到底是什么"的必经之路。

四、动手 Demo:三个常用套路

#include <iostream>
using namespace std;

int main() {
    int a[6] = {12, 45, 7, 33, 88, 20};
    int n = sizeof(a) / sizeof(a[0]);        // 数格子(7.1.1)

    cout << "正向:";
    for (int i = 0; i < n; i++) cout << a[i] << " ";
    cout << endl;

    cout << "倒序:";
    for (int i = n - 1; i >= 0; i--) cout << a[i] << " ";   // 从最后一格往回走
    cout << endl;

    int mx = a[0];                            // 找最大:从第一格起步
    for (int i = 1; i < n; i++)
        if (a[i] > mx) mx = a[i];             // 4.2.1 的单分支 if
    cout << "最大 = " << mx << endl;

    int pos = 0;                              // 找目标所在的下标
    for (int i = 1; i < n; i++)
        if (a[i] == 88) pos = i;
    cout << "88 的下标 = " << pos << endl;
    return 0;
}

看到什么算成功:

正向:12 45 7 33 88 20
倒序:20 88 33 7 45 12
最大 = 88
88 的下标 = 4

三个套路值得记下来:① 正向遍历从 0 到 n-1;② 倒序遍历从 n-1 到 0(下标是 i >= 0,别写成 i > 0,否则会漏掉第一格);③ "找最大/找目标"都把第一格当作初始值,从第二格开始比。

30 秒实验:把倒序循环的条件 i >= 0 改成 i > 0,重跑——最小的那个数(第一格 12)不见了。这就是"边界差一"(off-by-one)的典型现场,数组里天天遇到。

五、常见误区

  1. "越界会报错,报错我就知道错了"——不会报错。编译能过、运行可能也不崩,只是默默改坏别人的内存。越界是本课最"阴"的坑。
  2. "a[1] 是第一个元素"——是第二个。第一个是 a[0]。写循环时记住"数量减一"。
  3. "遍历写 i <= n 也行"——那就访问了 a[n],越界。应该是 i < n。
  4. "a[i] 只能读、不能改"——能改。a[i] = 值; 完全合法(它是左值,3.4)。数组的"不能整体赋值"(a = b 不行)和"单格能赋值"是两回事。
  5. "下标能给负数吗"——语法上 a[-1] 编译能过,但那是越界访问 a 前面的内存,属于未定义行为,后果不可预测。
自测三问(点开对答案) 1. `int a[5]` 中 `a[5]` 会怎样?——越界。编译器不报错,可能悄悄改坏相邻内存,属于未定义行为。 2. 长度为 n 的数组,遍历时的循环条件?——`for (int i = 0; i < n; i++)`,注意是 `<` 不是 `<=`。 3. 为什么 `a[i] = 5;` 合法,而 `a = b;` 不合法?——前者改的是**某一格**(有地址的左值);后者想整体替换**整排柜子的首地址**,而数组名不是可改的变量(7.5.1 细说)。

相关笔记