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

二维数组有两层地址,所以有两种指针:a 是"行指针"——a + 1 跳的是一整行(4 个 int = 16 字节);a[0] 是"元素指针"——a[0] + 1 只跳一个元素(4 字节)。两者打印出来的地址值相同,但类型不同,不能混用。下标法的完整展开式是:a[i][j] ≡ *(*(a+i)+j)。

📝 本节和前面几节的关系

上一节讲一维数组"四种写法等价",二维只是多套了一层。三个伏笔在这里同时回收:7.2.1 的"行优先连续存放"(让"一重循环走遍全表"成为可能)、7.4.3 的"指针加 1 跳一格"(现在要分清跳的是"格"还是"行")、7.5.1 的"数组名是首地址"(a 和 a[0] 都是地址,只是层级不同)。

一、两种指针:行指针 vs 元素指针

int a[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};
写法 类型 +1 跳多少 叫法
a int (*)[4] 16 字节(一整行) 行指针
a[0] int * 4 字节(一个元素) 元素指针
a[1] int * 4 字节 第 1 行的元素指针

为什么 a 是"行指针"?因为 C++ 把二维数组看成"一个数组,每个元素是一整行"。所以 a 指向的是"第 0 行这个整体",a + 1 自然就跳到"第 1 行这个整体"。

关键而反直觉的一点:

cout << a     << endl;     // 0x61ff00
cout << a[0]  << endl;     // 0x61ff00   ← 打印出来一模一样!

地址值相同,但类型不同(一个是 int(*)[4]、一个是 int*)。所以:

int *p = a;          // ❌ 编译警告/错误:类型不匹配(行指针 → 元素指针)
int *p = a[0];       // ✅ 正确:a[0] 就是元素指针

二、下标法的完整展开

a[i][j]  ≡  *(*(a + i) + j)

分三步读(从里往外):

步骤 表达式 含义
① a + i 走到"第 i 行这个整体"的地址
② *(a + i) 把行"取出来",得到第 i 行的元素指针(就是 a[i])
③ *(a + i) + j 从这行开头再往后挪 j 个元素
④ *(*(a+i)+j) 解引用,取到那一格的值

注意中间那个 * 不是"取值",而是"降一级"——把"行指针"降成"元素指针"。这是二维数组最绕的地方,多读几遍上面的四步表。

三、遍历的三种写法

写法 1:双层下标(最清楚,7.2.2 的老办法)

for (int i = 0; i < 3; i++)
    for (int j = 0; j < 4; j++)
        cout << a[i][j] << " ";

写法 2:行指针 + 元素指针(最"指针")

for (int (*row)[4] = a; row < a + 3; row++)          // row 是行指针
    for (int *col = *row; col < *row + 4; col++)     // *row 降级成元素指针
        cout << *col << " ";

int (*row)[4] 的括号不能省——它就是"行指针"的声明写法。省了括号变成 int *row[4],意思是"4 个指针组成的数组",完全是另一样东西(7.7 会讲)。

写法 3:一重循环走遍全表(靠"行优先连续")

int *p = &a[0][0];                // 表头的元素指针
for (int k = 0; k < 3 * 4; k++)
    cout << *(p + k) << " ";

为什么可以这么干?因为 7.2.1 说过:二维数组在内存里就是一排连续的。所以只要拿到表头的元素指针,一路往后走 12 格,就能走遍整张表——不需要记得"行"的存在。

四、实例10:使用指针变量遍历二维数组

#include <iostream>
using namespace std;

int main() {
    int a[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};

    cout << "---- ① 看两种指针 ----" << endl;
    cout << "a     = " << a    << endl;     // 地址值与 a[0] 相同
    cout << "a[0]  = " << a[0] << endl;
    cout << "a+1 - a(字节)   = " << (char*)(a+1)   - (char*)a   << endl;   // 16
    cout << "a[0]+1 - a[0](字节)= " << (char*)(a[0]+1) - (char*)a[0] << endl; // 4

    cout << "---- ② 行指针 + 元素指针 ----" << endl;
    for (int (*row)[4] = a; row < a + 3; row++) {
        for (int *col = *row; col < *row + 4; col++)
            cout << *col << "\t";
        cout << endl;
    }

    cout << "---- ③ 一重循环走遍全表 ----" << endl;
    int *p = &a[0][0];
    for (int k = 0; k < 12; k++) cout << *(p + k) << " ";
    cout << endl;

    cout << "---- ④ 下标法与指针法等价 ----" << endl;
    cout << "a[1][2]      = " << a[1][2] << endl;          // 7
    cout << "*(*(a+1)+2)  = " << *(*(a+1)+2) << endl;      // 7
    return 0;
}

看到什么算成功:

---- ① 看两种指针 ----
a     = 0x61fef0
a[0]  = 0x61fef0      ← 地址相同
a+1 - a(字节)   = 16
a[0]+1 - a[0](字节)= 4
---- ② 行指针 + 元素指针 ----
1   2   3   4
5   6   7   8
9   10  11  12
---- ③ 一重循环走遍全表 ----
1 2 3 4 5 6 7 8 9 10 11 12
---- ④ 下标法与指针法等价 ----
a[1][2]      = 7
*(*(a+1)+2)  = 7

三处必须看懂的:

输出 说明
a 和 a[0] 地址相同 都指向表头,但类型不同
a+1 - a 是 16 行指针跳一整行
a[0]+1 - a[0] 是 4 元素指针跳一格
③ 一重循环 证明"行优先连续":12 个元素真的排成一条线

30 秒实验:把 ③ 里的 int *p = &a[0][0]; 改成 int *p = a[0];,重跑——结果完全一样。因为 a[0] 和 &a[0][0] 是同一个地址的两种说法。再试试改成 int *p = a;——编译报错,因为 a 是行指针,类型不匹配。

五、常见误区

  1. "a 和 a[0] 是一回事,反正地址一样"——类型完全不同。a 是 int(*)[4](跳行),a[0] 是 int*(跳格)。写 int *p = a; 会报错。
  2. "a+1 是跳到下一个元素"——a+1 跳一整行(16 字节)。要跳一个元素得用 a[0]+1。
  3. "int (*row)[4] 的括号可有可无"——必须有。去掉括号就变成 int *row[4](4 个指针的数组),是 7.7 的"指针数组",完全另一回事。
  4. "遍历二维数组必须用两层循环"——不一定。行优先连续意味着一层循环 + 一个元素指针就能走遍全表(写法 3)。
  5. "*(a+i) 的 * 是取第 i 行的值"——它是"把行指针降级成元素指针",拿到的不是某一格的值,而是"那一行开头的地址"。
自测三问(点开对答案) 1. `a[i][j]` 的完整指针写法是什么?——`*(*(a + i) + j)`。 2. `a` 和 `a[0]` 都打印同一个地址,能互换使用吗?——不能。`a` 是行指针(`int(*)[4]`),`a[0]` 是元素指针(`int*`),类型不同。 3. 为什么一重循环就能走遍二维数组?——因为二维数组在内存里**按行优先连续存放**,拿表头的元素指针一路 +1 即可。

相关笔记