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

第七章是全书从"用变量"跨到"用内存"的分水岭。三块拼图:数组(一排柜子:同类型、连续、下标从 0)、指针(记门牌号的纸条:& 取地址、* 取值、按元素挪动)、引用(别名:更安全的指针)。它们最终都汇成一句话——"想改原件,就把地址交出去"。

📝 本节和前面几节的关系

这一章把前面六章的所有伏笔一次收完:2.3.2 的"未初始化=垃圾值"(→ 野指针)、3.1.7 的 sizeof(→ 数组名与指针的区别)、3.4 的左值(→ *p 能写)、6.3.1 的 swap 换不动(→ 7.6.1 和 7.8.2 两次回收)。下一章(第 8 章)会在这个地基上继续往上盖——结构体、类、动态内存,全都建立在"地址"之上。

一、本章知识地图

块 小节 一句话要点
一维数组 7.1 / 7.1.1 / 7.1.2 / 7.1.3 一排连号柜子:同类型 · 连续 · 下标从 0;越界不报错;int a[5] = {}; 一键清零
二维数组 7.2 / 7.2.1 / 7.2.2 / 7.2.3 一张表,底层仍是行优先的一排;a[i][j] 要两对方括号;列数不能省
字符数组 7.3 字符串 = 末尾带 '\0' 的字符数组;四大函数 strcat / strcpy / strcmp / strlen(都不能用运算符代替)
指针 7.4 ~ 7.4.5 存地址的变量;& 取地址、* 解引用(*p 是左值,能改原件);运算按"元素"计数;空指针 nullptr;void * 万能但不解引用;const 在 * 左边锁值、右边锁指针
指针与数组 7.5 / 7.5.1 / 7.5.2 / 7.5.3 数组名 = 首地址;a[i] ≡ *(a+i);二维有两层指针(a 跳行、a[0] 跳格);字符数组靠 '\0' 自动停
指针与函数 7.6 / 7.6.1 / 7.6.2 / 7.6.3 传地址能改原件(swap 终于写对了);函数指针把代码当数据传;返回指针绝不能返回局部变量地址
指针数组 7.7 int *p[5](5 个指针)vs int (*p)[5](1 个指针),看括号
引用 7.8 / 7.8.1 / 7.8.2 / 7.8.3 别名:必须初始化、不能改绑、不能为空;引用传参写出最漂亮的 swap;数组传参会退化成指针(长度必须另传)

二、必须记住的六个结论

  1. 数组:同类型、连续存放、下标从 0。a[n] 越界不报错——这是本章最阴的坑,会悄悄改坏别的内存。
  2. 数组名就是首地址,但它不是变量。不能 a++、不能赋值;sizeof(a) 是整排容量,sizeof(p) 只是纸条大小。
  3. 指针的两个符号:& 取地址、* 解引用。*p 是左值——所以 *p = 99; 能改原件。指针加减按"元素"计数(int* 加 1 跳 4 字节)。
  4. 二维数组有两层指针:a 是行指针(+1 跳一整行),a[0] 是元素指针(+1 跳一格);a[i][j] ≡ *(*(a+i)+j)。
  5. 想改原件,有三种写法:传值(改不动)、传指针(*a + &x)、传引用(最干净)。只读大对象用 const T &x。
  6. 数组作参数会退化成指针:函数里改数组 = 改原件,但 sizeof 失效 → 长度必须另传一个参数。

三、动手 Demo:两关自测

第一关(写程序):用指针做完三件事

要求:写三个函数——① 用指针把数组逆序;② 用指针找最大值并返回它的地址;③ 用引用写一个 swap,并在一趟冒泡里用它。

#include <iostream>
using namespace std;

void reverseArr(int *a, int n) {                 // ① 指针逆序
    int *left = a, *right = a + n - 1;
    while (left < right) {
        int t = *left; *left = *right; *right = t;
        left++; right--;
    }
}

int *findMax(int a[], int n) {                   // ② 返回最大值的地址
    int *p = &a[0];
    for (int i = 1; i < n; i++)
        if (a[i] > *p) p = &a[i];
    return p;                                    // 安全:指向调用方的数组
}

void swapRef(int &a, int &b) {                   // ③ 引用版 swap
    int t = a; a = b; b = t;
}

void bubbleSort(int a[], int n) {                // 用 swapRef 做一趟冒泡
    for (int i = 0; i < n - 1; i++)
        for (int j = 0; j < n - 1 - i; j++)
            if (a[j] > a[j + 1]) swapRef(a[j], a[j + 1]);
}

int main() {
    int arr[8] = {12, 45, 7, 33, 88, 20, 5, 61};

    reverseArr(arr, 8);
    cout << "逆序后:";
    for (int i = 0; i < 8; i++) cout << arr[i] << " ";

    cout << endl << "最大值 = " << *findMax(arr, 8)
         << ",它在第 " << (findMax(arr, 8) - arr) << " 格" << endl;

    bubbleSort(arr, 8);
    cout << "排序后:";
    for (int i = 0; i < 8; i++) cout << arr[i] << " ";
    cout << endl;
    return 0;
}

看到什么算成功:逆序后是 61 5 20 88 33 7 45 12;最大值 88,在第 3 格;排序后是 5 7 12 20 33 45 61 88。

检查清单: - ① 逆序用的是首尾双指针夹逼(7.5.1 的写法 2),没用到下标; - ② 返回的是数组内部的地址(安全),调用方用 * 取值、用指针相减算下标; - ③ swapRef 调用时没写 &——这是引用版的特征(7.8.2)。

第二关(找错):六处问题

下面这段有六处问题,找出来并说明原因:

void swap(int a, int b);                     // ①
int *bad() {
    int local = 5;
    return &local;                           // ②
}

int main() {
    int arr[5] = {3, 1, 4, 1, 5};
    int *p = arr;
    p++;
    cout << arr[5] << endl;                  // ③
    int *q;
    cout << *q << endl;                      // ④
    char *s = "hello";
    s[0] = 'H';                              // ⑤
    swap(arr[0], arr[1]);
    return 0;
}

答案与理由:

# 问题 原因与修法
① swap(int a, int b) 是传值 换不动(6.3.1)。改 void swap(int *a, int *b) 配 swap(&x,&y),或 void swap(int &a, int &b) 配 swap(x,y)(7.8.2)
② 返回局部变量的地址 local 随函数结束被回收(6.7.1),成为悬空指针。改成返回 static 变量、全局变量或"传入的数组"(7.6.3)
③ arr[5] 越界 5 个元素的合法下标是 0~4。不报错,但会踩坏相邻内存(7.1.2)
④ q 未初始化就解引用 野指针(7.4.4)。*q 读的是随机内存。应先 int *q = nullptr; 并判空,或先赋值
⑤ 改字符串常量 s 指向只读区,s[0]='H' 运行时崩溃(7.5.3)。要改写:char s[] = "hello";
⑥ —— 其实还有一处隐蔽的:p++ 之后 p 再没被用过 不算错,但说明没用。真正的坑是:p++ 之后若还想用 arr[0] 的位置,必须复位 p = arr;(7.5.1 的"指针跑过头")

30 秒实验:把修正后的程序跑一遍,确认逆序、排序、交换都正确;再故意把 char s[] 改回 char *s,看它编译能过、运行崩溃——亲身体会"能编译 ≠ 能运行"。

四、常见误区(本章汇总)

  1. "编译通过 = 没问题"——本章几乎每个坑都是"编译能过、运行才出事":越界、野指针、悬空指针、改字符串常量、sizeof 失效。
  2. "数组和指针是一回事"——sizeof 不一样、数组名不能自增。"数组名退化成指针"只发生在特定场合(传参、赋值给指针)。
  3. "指针只是语法,理解了就好"——必须动手看地址。本章每节都有"打印地址"的实验,没亲手跑过,就永远记不住。
  4. "引用和指针差不多,随便选"——语义不同:引用"一定有效"(不能为空、不能改绑),指针"可以没有目标"(nullptr)。要表达"可能没有",只能用指针。
  5. "一改就报错的东西才叫危险"——反了。越界、野指针这类"不报错"的才是真危险。本章反复在教同一件事:C++ 把自由都给你了,把关也得你自己来。
自测六问(点开对答案) 1. 数组的三个特征?——同类型、连续存放、下标从 0。 2. `a[i]` 的指针写法?——`*(a + i)`。 3. `int *p` 里 `p + 1` 地址加多少?——加 `sizeof(int)` 字节,也就是 4(**按元素算,不是按字节**)。 4. 二维数组里 `a` 和 `a[0]` 有什么不同?——`a` 是行指针(`+1` 跳一整行),`a[0]` 是元素指针(`+1` 跳一个元素);**地址相同,类型不同**。 5. 想改原件,传指针和传引用各有什么特点?——都能改;**引用写起来更干净**(调用处不用 `&`、体内不用 `*`),指针**能表示"空"**。 6. 数组作函数参数时,为什么必须另传长度?——因为数组退化成指针,函数里的 `sizeof(a)` 得到的是指针大小,算不出个数。

相关笔记