二维数组有两层地址,所以有两种指针: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 是行指针,类型不匹配。
五、常见误区
- "
a和a[0]是一回事,反正地址一样"——类型完全不同。a是int(*)[4](跳行),a[0]是int*(跳格)。写int *p = a;会报错。 - "
a+1是跳到下一个元素"——a+1跳一整行(16 字节)。要跳一个元素得用a[0]+1。 - "
int (*row)[4]的括号可有可无"——必须有。去掉括号就变成int *row[4](4 个指针的数组),是 7.7 的"指针数组",完全另一回事。 - "遍历二维数组必须用两层循环"——不一定。行优先连续意味着一层循环 + 一个元素指针就能走遍全表(写法 3)。
- "
*(a+i)的*是取第 i 行的值"——它是"把行指针降级成元素指针",拿到的不是某一格的值,而是"那一行开头的地址"。
自测三问(点开对答案)
1. `a[i][j]` 的完整指针写法是什么?——`*(*(a + i) + j)`。 2. `a` 和 `a[0]` 都打印同一个地址,能互换使用吗?——不能。`a` 是行指针(`int(*)[4]`),`a[0]` 是元素指针(`int*`),类型不同。 3. 为什么一重循环就能走遍二维数组?——因为二维数组在内存里**按行优先连续存放**,拿表头的元素指针一路 +1 即可。- ☐ 7.5.3 指针与字符数组(下一节)
相关笔记
- 指针与一维数组 —— 两层里先搞懂第一层
- 二维数组的声明 —— "行优先连续"的出处
- 指针运算 —— 跳格还是跳行,全看类型
- 指针数组 ——
int *p[4]与int (*p)[4]的区别 - 🔷 C++基础 · 索引 —— 全课路线图