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

让指针指向数组的第一个元素(int *p = a;),然后就有四种完全等价的取元素方式:a[i]、*(a+i)、p[i]、*(p+i)。更妙的是——指针自己就能一步步往前走:*p++ 先取值、再把指针挪一格,一趟循环走完整排柜子。但千万记住 sizeof 的区别:数组名是全排,指针是一张纸条。

📝 本节和前面几节的关系

7.5 讲了"数组名就是首地址、a[i] ≡ *(a+i)"。这一节把它落到实处:写成可运行的遍历。用到的零件:7.4.3 的"指针加 1 跳一格"、3.2 的优先级(*p++ 谁先算)、7.1.2 的越界坑(指针走过头同样越界)。这是本章第一个"指针真正好用"的地方。

一、让指针指向数组

int  a[5] = {10, 20, 30, 40, 50};
int *p = a;          // 指向 a[0](等价于 &a[0])

此时 p 和 a 指的是同一处——都可以用来访问这排柜子。

二、四种等价写法

int  a[5] = {10, 20, 30, 40, 50};
int *p = a;

a[2]        // 30
*(a + 2)    // 30    ← 数组名当指针用
p[2]        // 30    ← 指针当数组用
*(p + 2)    // 30

为什么能互相替代?因为三者都是同一个算式:首地址 + 偏移 再解引用。

写法 拆解 说明
a[i] 编译器自动翻译成 *(a + i) 下标法是"语法糖"
*(a + i) 首地址 + i 格,再取值 本质写法
p[i] 编译器自动翻译成 *(p + i) 指针也能用下标
*(p + i) 同 ② p 和 a 此时指向相同

记住这一句就够了:[] 只是 *( + ) 的简写。

三、用指针遍历:两种经典写法

写法 1:指针自增(最"指针"的写法)

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

for (int i = 0; i < 5; i++) {
    cout << *p << " ";
    p++;                    // 指针往后走一格
}

更紧凑的写法是 *p++——它在一行里干两件事:

for (int i = 0; i < 5; i++)
    cout << *p++ << " ";

*p++ 到底怎么算?(3.2 的优先级在这里上岗)

写法 拆解 结果
*p++ *(p++):先取值,再把 p 挪一格 打印当前格的值
(*p)++ 先取 *p,再把那个值加 1 把当前格的值 +1
++*p ++(*p):把当前格的值 +1 同上一行

注意 *p++ 里 ++ 优先于 *,所以它是 *(p++) 而不是 (*p)++。这两个写法结果完全不同,是本节最需要动手验证的一处。

写法 2:首尾指针夹逼(更清晰)

int *begin = a;
int *end   = a + 5;         // 指向"最后元素之后"的位置

for (int *q = begin; q < end; q++)
    cout << *q << " ";

这种写法不用数 i,靠"指针有没有走到尾"来判断。a + 5 是尾后位置(第 5 格之后,也就是第 6 格)——它本身不能解引用,只用来当"终点线"。

四、实例09:通过指针变量获取数组中的元素

#include <iostream>
using namespace std;

int main() {
    int  a[5] = {10, 20, 30, 40, 50};
    int *p = a;                              // 指向第一个元素

    cout << "---- ① 四种写法对照 ----" << endl;
    for (int i = 0; i < 5; i++) {
        cout << a[i] << " " << *(a+i) << " " << p[i] << " " << *(p+i) << endl;
    }

    cout << "---- ② 指针自增遍历 ----" << endl;
    p = a;                                    // 别忘了让 p 回到开头!
    for (int i = 0; i < 5; i++) cout << *p++ << " ";
    cout << endl;

    cout << "---- ③ 首尾夹逼遍历 ----" << endl;
    for (int *q = a; q < a + 5; q++) cout << *q << " ";
    cout << endl;

    cout << "---- ④ 指针相减求元素个数 ----" << endl;
    int  *first = a;
    int  *last  = &a[4];
    cout << "元素个数 = " << (last - first) + 1 << endl;    // 5
    return 0;
}

看到什么算成功:

---- ① 四种写法对照 ----
10 10 10 10
20 20 20 20
30 30 30 30
40 40 40 40
50 50 50 50
---- ② 指针自增遍历 ----
10 20 30 40 50
---- ③ 首尾夹逼遍历 ----
10 20 30 40 50
---- ④ 指针相减求元素个数 ----
元素个数 = 5

三处千万注意:

  1. ② 之前必须 p = a;——上一段循环里 p 已经被推到末尾了(这就是 *p++ 的副作用)。忘了复位,第二段会打印垃圾。
  2. ③ 的终止条件是 q < a + 5(不是 <=)——a + 5 是尾后位置,解引用它就越界。
  3. ④ 用 last - first + 1 算元素个数:指针相减得到"相隔几格"(7.4.3),加 1 才是总数。

30 秒实验:把 ② 里的 *p++ 改成 (*p)++,重跑——输出会变成 11 21 31 41 51,而且打印的是原值、格子里已经加过 1 了。再把数组打印一遍,会看到 a 真的被改成 {11,21,31,41,51}。一行之差,一个只是"走过",一个是"改掉"。

五、常见误区

  1. "数组名和指针完全一样"——sizeof 不一样(7.5):sizeof(a) 是 20、sizeof(p) 是 4。数组名也不能 a++。
  2. "*p++ 是先把值加 1"——反了。是"先取值,再把指针挪一格"。要改值得写 (*p)++。
  3. "指针遍历完还能接着用"——循环结束时 p 已经指向末尾之后,再用它就越界。要么中途复位,要么另开一个指针。
  4. "a + 5 就是最后一个元素"——a + 5 是尾后位置(第 5 格之后),不能解引用;最后一个元素是 a[4](a + 4)。
  5. "指针访问数组比下标快很多,所以都该用指针"——现代编译器会把 a[i] 优化成同样的代码,性能差别可以忽略。选哪种看可读性:a[i] 更适合日常,指针遍历更适合"边走边改地址"的场景。
自测三问(点开对答案) 1. `p[3]` 和 `*(p + 3)` 有关系吗?——**完全等价**,编译器把 `p[3]` 翻译成 `*(p + 3)`。 2. `*p++` 和 `(*p)++` 有什么不同?——前者"取值 + 指针挪一格",后者"把当前格的值加 1"。 3. 指针遍历结束后,为什么不能直接再遍历一遍?——因为指针已经跑到末尾之后了,**要复位到开头**(`p = a;`)。

相关笔记