多维数组指针

官方信息技术老师·21 页·深入(追求细节与边界)·0 次浏览·2 天前
内存布局指针运算数组降维边界陷阱

多维数组指针

看完你能亲手画出任意维度数组的内存布局并解释指针运算

按 空格/→ 演示下一步

1 / 21 页

全部页面点击任意一页,跳回舞台从这页播放

内存布局指针运算数组降维边界陷阱

多维数组指针

看完你能亲手画出任意维度数组的内存布局并解释指针运算

1第 1 页 · 多维数组指针

一维数组的内存回顾

连续内存、元素地址、数组名本质

一维数组的内存回顾
连续内存、元素地址、数组名本质
2第 2 页 · 一维数组的内存回顾

二维数组的内存布局

左侧为二维逻辑视图,右侧为物理上的连续内存,展示行优先拼接与地址公式。

图解渲染中…
a1程序员眼中的二维网格(行×列)b1内存条上的连续字节块c2元素 a[i][j] 相对首地址的偏移公式
3第 3 页 · 二维数组的内存布局

多维数组的线性存储

从三维逻辑结构出发,经行优先偏移公式计算,最终摊平为连续的一维内存。

图解渲染中…
a1声明三维数组,三个维度大小为 2/3/4b1逻辑上是 2 层、每层 3 行、每行 4 列d1offset = i*D2*D3 + j*D3 + ke1共 2*3*4 = 24 个连续内存单元
4第 4 页 · 多维数组的线性存储

行优先 vs 列优先

行优先和列优先名字像、规则反,混用时偏移计算直接错位。

C 语言·行优先
  • 先存完第 0 行的所有元素,再接第 1 行
  • a[i][j] 偏移 = i × 列数 + j
  • 遍历时外层走行、内层走列更缓存友好
Fortran 列优先
  • 先存完第 0 列的所有元素,再接第 1 列
  • a(i,j) 偏移 = j × 行数 + i
  • 遍历时外层走列、内层走行更缓存友好
C 是行优先:外层行、内层列遍历最命中缓存;调用 Fortran 矩阵库时一定要手动转置。
5第 5 页 · 行优先 vs 列优先

数组名作为指针常量

前页我们看到二维数组在内存里也是按行线性排开的,那 arr 这个名字本身究竟指向哪里?它跟一维数组里的 arr 含义完全一样吗?

数组名是常量
编译期就定死的地址,不能被赋值或 ++
二维数组名指向首行
arr 等价于 &arr[0],地址值就是首行起点
类型是行指针
int (*)[N],+1 时跳过整行 N 个元素
与一维数组名不同
一维指向单个元素,二维指向整行
楼栋编号对应 →二维数组名

楼号对首行(一整层),门牌号对单户;跳一号跳整行

arr&arr[0],  type=int()[extN]\text{arr}\equiv\&\text{arr}[0],\;\text{type}=\text{int}(*)[ ext{N}]
6第 6 页 · 数组名作为指针常量

多维数组指针表达式的等价变形

从 a[i][j] 出发,逐步拆解下标的指针本质,得到纯指针运算形式。

1
从下标写法开始
a[i][j] 是数组下标语法,可读性最好
2
内层 a[i] 展开
下标等价于指针解引用:a[i] ≡ *(a+i)
3
外层再展开一次
把 *(a+i) 代入 a[i],得到 *(*(a+i)+j)
4
注意类型差异
a 是 int(*)[N] 类型,+i 跳整行,+j 跳元素
7第 7 页 · 多维数组指针表达式的等价变形

数组名 vs 指针变量

值相同不代表类型相同——a 是常量、p 是变量,混淆就会越界或编译报错。

数组名 a
  • 本质是常量指针,地址固定不变
  • 不可赋值:a++、a=p 都非法
  • sizeof(a) 得整个数组的字节数
指针变量 p
  • 本质是存储地址的变量,值可变
  • 可赋值:p++、p=a 都合法
  • sizeof(p) 得指针本身的大小
a 是常量、p 是变量:值相同时,类型决定合法操作——遍历用 p,固定结构用 a。
8第 8 页 · 数组名 vs 指针变量

验证 a 与 &a[0] 的关系

c

用一段 C 程序把 a 与 &a[0] 打印出来,对比它们在地址值与步长上的关系。

代码高亮加载中…

a 与 &a[0] 值相同,但 a+1 跳一整行(16 字节),*a+1 只跳一个 int(4 字节)——这是行优先遍历的指针基础。

9第 9 页 · 验证 a 与 &a[0] 的关系

数组指针的本质

上一页我们看到 a 和 &a 的值相同,C 却用两种类型去描述它们——看似多余,恰恰是数组指针区别于普通指针的关键。

类型刻着列长度
int (*p)[4] 里那个 4 写在类型里,不是变量,也不是别名
步长由类型决定
p+1 一次性跳 4 个 int,正好跨过一整行
解引用是整个数组
*p 等价于 int[4],可当一维数组名,sizeof 拿到整行字节
与元素指针不可混用
int* 和 int(*)[4] 类型不同,不能互相赋值,需显式强转
印着排座数的长途车票对应 →数组指针类型

车票写明每排4座,类型印着 [4],步长直接由此决定

10第 10 页 · 数组指针的本质

数组指针的声明与使用

c

让 p 指向二维数组的“行”,验证步长与三种访问写法。

代码高亮加载中…

高亮行串起核心:括号决定 p 是“行指针”,步长 = sizeof(int[3]) = 12;(*p)[i]、p[0][i]、(*(p+1))[i] 三种写法等价。

11第 11 页 · 数组指针的声明与使用

数组指针 vs 指针数组

两个长得像、优先级相反的概念,括号位置决定一切。

数组指针 (*p)[n]
  • 本质是指针:指向含 n 个元素的整数组
  • 优先级:() 优先 → p 先与 * 结合
  • 步长 = n×sizeof(int),p+1 跳一整行
指针数组 *p[n]
  • 本质是数组:含 n 个元素,每个是 int*
  • 优先级:[] 优先于 * → p 先与 [] 结合
  • 步长 = sizeof(int*),p+1 跳一个指针
看括号套在谁身上。(*p)[n] 是「指向数组的指针」,*p[n] 是「装着指针的数组」——一字之差,主语颠倒。
12第 12 页 · 数组指针 vs 指针数组

二维数组参数的三种声明形式

c

三种声明在编译器眼里究竟是不是一回事?跑一下就知道。

代码高亮加载中…

写法 1 在参数调整后与写法 2 类型完全相同;写法 3 退化为 int*,行列维度信息丢失,只能手动算偏移。

13第 13 页 · 二维数组参数的三种声明形式

为什么需要指向数组的指针

上一节我们看了三种写法都能接收二维数组:int a[][5]、int (*a)[5]、甚至二维指针。但写法不同,生成的机器码真的相同吗?答案是——形参写什么,编译器就怎么算地址。

形参决定步长
编译器读形参类型,确认一行有 N 个元素,按 i×N+j 算偏移
地址翻译公式
a[i][j] 最终被展开为 *((int*)a + i*N + j),N 由形参给出
写错就乱套
若形参是 int**,步长变 1,编译器算出的地址彻底错位
酒店前台查房间号对应 →形参的第二维大小

不告诉前台每层几间房,305 既是 3 楼 05 号,也是 30 楼 5 号

a[i][j]((int)a+i×N+j)a[i][j] \equiv *((\text{int}*)a + i \times N + j)
14第 14 页 · 为什么需要指向数组的指针

指向数组的指针作为形参

c

完整演示 int (*p)[col] 作形参:p+1 跳整行,p[i][j] 两层解引用。

代码高亮加载中…

形参 int (*p)[3] 是指向含 3 个 int 数组的指针;p+1 跨 12 字节(一整行);p[i][j] 即 *(*(p+i)+j);调用时 a 自动退化为该类型。

15第 15 页 · 指向数组的指针作为形参

二维数组函数参数传递的内存视图

从调用现场到形参内部,看传递瞬间的地址与维度信息取舍。

图解渲染中…
C列数4编入指针类型,p+1跨过4个intD行数3不在类型里,需另传参数如int nEint a[][4] 与 int (*a)[4] 等价
16第 16 页 · 二维数组函数参数传递的内存视图

二维数组的指针算术规则

把 a[i][j] 拆成纯指针运算,一步步看清每步在干什么。

1
从 a 出发
a 指向「含 4 个 int 的数组」,是行指针
2
a + i 跳行
加 i 跳 i 行,步长为 sizeof(int[4])
3
*(a+i) 取整行
解引用得到第 i 行,类型仍为 int[4]
4
*(a+i) + j 跳列
在第 i 行内部继续偏移 j 列
5
再解引用取值
取出 a[i][j] 这个具体的 int 元素
17第 17 页 · 二维数组的指针算术规则

用指针遍历二维数组

c

对比双重下标与纯指针算术两种遍历二维数组的方式,并验证它们访问同一块内存。

代码高亮加载中…

L5 取首元素地址;L10/12 双重循环按下标访问;L17/18 单层循环按指针偏移访问;L22 证明 a[1][2] 与 *(p+5) 指向同一地址。

18第 18 页 · 用指针遍历二维数组

常见错误与边界情况

三类错误症状都像「指针乱跑」,但病在边界、类型、步长三个不同维度。

常见错误做法
  • p++ 一路扫到末尾,就以为越界
  • int* p 当 int(*)[N] 用,步长不一致
  • p += 1 在不同类型上移的距离不同
正确理解
  • 越界看类型步长,分清越过哪一维
  • 类型决定步长,混用必然错位
  • 一维用 T*,二维用 T(*)[N]
诊断先看症状:编译错是类型、崩溃是越界、值错位是步长。三者病因不同,不能混治。
19第 19 页 · 常见错误与边界情况

多维数组指针要点回顾

  • 内存本质是一维连续的线性空间
  • 指针类型决定了 +1 跨越的元素数
  • 下标访问等价于指针的解引用
  • 传参时高维信息会部分丢失
  • 类型不匹配是越界访问的常见根因
延伸主题:动态分配二维数组指针运算的边界检查函数指针与回调
20第 20 页 · 多维数组指针要点回顾

课后思考

先自己琢磨再看参考答案,三道题对应核心、用法与边界。

1int (*p)[4] 中的圆括号为什么不能省?去掉括号含义有何根本变化?

参考答案圆括号决定 `*` 与 `p` 哪个先结合。`int (*p)[4]` 是指向含 4 个 int 数组的指针;`int *p[4]` 是装着 4 个 int 指针的数组——一个是指针,一个是数组。

2实参分别传 `a`、`a[0]`、`&a[0][0]`,哪个能与形参 `int (*p)[4]` 匹配?

参考答案`a` 退化为 `int (*)[5]`,列数不匹配通常报错;`a[0]` 退化为 `int*`,类型完全不符,必报错;`&a[0][0]` 也是 `int*`,同样不匹配。

3若 a 实为 3×5,却用 `int (*p)[4]` 接收,编译与运行各会怎样?

参考答案多数编译器会警告不匹配。强行运行后 `*p` 按 4 列步进访问,列偏移错位,越界踩到临近内存,结果难料。

21第 21 页 · 课后思考