指针与指针

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

指针数组

看完你能画出指针数组的内存图,并解释它如何存一组字符串

按 空格/→ 演示下一步

1 / 21 页

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

指针数组内存布局字符串数组

指针数组

看完你能画出指针数组的内存图,并解释它如何存一组字符串

1第 1 页 · 指针数组

普通数组的局限

上页把指针装进数组,让"装地址"成了可能。但反观普通数组本身,它有三个硬伤锁死了灵活性。

元素类型固定
声明 int 就只能装 int,换类型得整数组重写
内存必须连续
所有元素挤在一块连续空间,中间缺一段就放不下
长度编译期固定
声明时多大就多大,运行时既不能长也不能缩
停车场对应 →普通数组

车位大小一致、必须排成一排、总数在建造时就定死

2第 2 页 · 普通数组的局限

场景引入:字符串数组

上一页我们看到普通二维数组存字符串,每个短串都得按最长串对齐,浪费大量空间。那有没有办法让每个字符串「刚好」占自己需要的空间?这就要用到字符串数组。

字符串数组
本质是 char* 数组,每个元素是指向字符串的指针
独立存储
各字符串可分散在常量区、栈或堆中
长度自由
各串长度独立,按需分配,不需对齐
图书馆带书签的书架对应 →字符串数组

书架上书签连续排列=指针数组;书本可放任意位置=字符串分散存储

3第 3 页 · 场景引入:字符串数组

普通数组 vs 指针数组

两者都能装多个字符串,但底层内存模型完全不同——这决定了能不能改。

普通数组 char[][]
  • 一整块连续内存,编译期确定大小
  • 按最大长度 M 分配,短串浪费空间
  • 元素可改写,在栈或全局区
指针数组 char*[]
  • N 个指针分散指向各字符串
  • 各串按实际长度分配,零浪费
  • 字面量只读,改写触发段错误
长度已知且需修改选左;只读引用、长度参差选右。
4第 4 页 · 普通数组 vs 指针数组

指针数组的定义

前页把字符串首地址交给数组后,问题落到“这些地址也能批量存放吗”?可以:先划出 5 个连续元素,再规定每个元素都是 int 指针。

结合顺序
[] 先与 arr 结合;arr 是含 5 个元素的数组
元素类型
去掉 arr[5] 后,元素声明类型为 int*
合法初值
元素应存 int 变量的地址,如 &x,不能直接存 int 值
访问与退化
arr[i] 得到地址;*arr[i] 取得 int,arr 常退化为 int**
五格快递柜对应 →int *arr[5]

五个格口对应 5 个元素,格内编码对应所存 int 地址

intarr[5]arr:array(5,int)\mathrm{int} * \mathrm{arr}[5] \Longleftrightarrow \mathrm{arr}:\mathrm{array}(5,\mathrm{int}*)
5第 5 页 · 指针数组的定义

指针数组的内存布局

指针数组本身连续存指针,每个指针再指向各自的字符串。两层结构:数组一层、字符串一层。

图解渲染中…
sg1数组本体在栈/全局区连续存放sg2字符串字面量在只读常量区a1arr[0] 是指针,存的是字符串首地址b1字符串本体是独立的一块内存
6第 6 页 · 指针数组的内存布局

指针数组的声明与初始化

c

用 sizeof 直接看穿 names[] 每个元素其实是个 8 字节的指针。

代码高亮加载中…

声明里 * 紧贴名字、[] 在后,说明 4 个元素都是指针;sizeof(names[0])=8 而非字符串长度,印证元素是指针;%s 通过解引用取出字符串。

7第 7 页 · 指针数组的声明与初始化

通过指针数组访问元素

c

同一组 names,两套语法都能拿到字符串首地址——关键在于 names 本身的类型。

代码高亮加载中…

L11 点明下标与指针的等价;L14 用 names[i] 取第 i 个元素;L19 注意 p 是二级指针,类型必须与 names 元素匹配;L21 用 *p 解引用一次得到字符串首地址。

8第 8 页 · 通过指针数组访问元素

字符串数组:const char *

c

一段可运行的 C 程序:声明 const char* 指针数组,并打印每个元素的指针地址、字符串与首字符。

代码高亮加载中…

指针数组存的是字符串地址,不是字符串本体。%s 从指针读到 \0 终止,*names[i] 解引用拿到首字符——const char* 字符串数组的访问方式即由此展开。

9第 9 页 · 字符串数组:const char *

字符串数组的内存模型

c

用代码把字符串数组的两层结构打出来:数组元素连续,指针指向只读段。

代码高亮加载中…

声明的数组在栈区连续,元素存的只是地址;这些地址指向 .rodata 里的字符串字面量,所以指针数组是「两级跳转」:先找指针,再找字面量。

10第 10 页 · 字符串数组的内存模型

函数指针数组

前页的指针数组把变量的地址排成一行;函数也有可调用的入口地址,因此可以把若干函数地址放进同一数组,按索引取得并调用目标函数,这正是函数指针数组的核心用途。

元素类型
每个元素都是函数指针,指向函数入口地址;函数本身不作为数组元素存放
声明形式
int (*fp[4])(int, int):含 4 个函数指针,元素接收两个 int 并返回 int
初始化调用
用函数名或兼容函数指针初始化;fp[1](a,b) 间接调用第二个目标函数
边界区别
元素保存地址而非函数对象;返回值和参数列表的类型必须兼容
典型用途
用统一接口组织菜单、回调或按编号分派不同处理函数
菜单编号对应 →函数指针数组

每个编号对应一个函数入口地址,按编号即可调用对应处理函数

int(fp[N])(int,int);fp[i](a,b)int (*fp[N])(int, int);\quad fp[i](a,b)
11第 11 页 · 函数指针数组

函数指针数组示例

c

用查表代替 if/else 链:把同签名的运算函数按顺序塞进数组,输入字符索引过去直接调用。

代码高亮加载中…

ops[i] 把字符索引翻译成函数调用——这就是分支 dispatch:让数据(运算符)驱动代码(函数)的选择,而不是一连串 if/else。

12第 12 页 · 函数指针数组示例

二级指针的定义

指针数组名 int *arr[3] 传给函数时,参数类型其实是 int **——比指针数组再多一层 *,专门用来保存'指针的地址'。

一级指针回顾
int *p 中 p 保存 int 变量地址,*p 取到 int 值
二级指针定义
int **ptr 中 ptr 保存 int* 变量的地址
两次解引用
*ptr 得 int*,**ptr 才得到最终的 int 值
步长为指针大小
ptr+1 跳 8 字节(一个 int*),不是 4 字节
典型用途
函数内改外部指针、指针数组名作参数
装着纸条的小盒子对应 →二级指针的多层间接

一级指针是写着房号的纸条;二级指针指向装纸条的那个盒子

int** ptr;ptr*ptr (int*)**ptr (int)\text{int** ptr;} \quad \text{ptr} \xrightarrow{*} \text{*ptr (int*)} \xrightarrow{*} \text{**ptr (int)}
13第 13 页 · 二级指针的定义

二级指针的内存模型

从上往下读:实线是『存了什么门牌号』,虚线是『解引用后等价于什么』。

图解渲染中…
a1二级指针变量,占地址 0x300b1一级指针变量,占地址 0x200c1目标整型变量,占地址 0x100d1x 实际存储的整数值 42
14第 14 页 · 二级指针的内存模型

二级指针的使用场景

c

用 int ** 在堆上搭出行列都可变的二维数组。

代码高亮加载中…

malloc 两层得到指针数组+数据块;matrix[i][j] 等价于 *(*(matrix+i)+j),体现「两次跳转」;free 必须反向,先行后表。

15第 15 页 · 二级指针的使用场景

二级指针作为函数参数

c

通过 &ptr 传入指针自身的地址,函数里就能改写调用方的指针变量本身。

代码高亮加载中…

L5、L6 是核心:用 *p 改写调用方指针本身,再用 **p 写入数据。L12 传 &ptr,把指针自己的地址交给函数。

16第 16 页 · 二级指针作为函数参数

指针数组即二级指针的雏形

图解两者在内存中的等价关系

指针数组即二级指针的雏形
图解两者在内存中的等价关系
17第 17 页 · 指针数组即二级指针的雏形

指针数组与二级指针对比

长得都像 `int**`,但数组是容器的视角,指针是地址的视角。

指针数组
  • 本质是数组:`int *arr[5]` 有 5 个元素
  • 元素在内存中连续存放,按下标访问
  • 适合字符串表、函数指针表等查表场景
二级指针
  • 本质是指针:`int **p` 只有 1 个变量
  • 指向另一个 int*,多一层间接寻址
  • 用于函数内修改外部指针、动态二维数组
要存一批指针用数组,要间接改指针本身或建指针链用二级指针。
18第 18 页 · 指针数组与二级指针对比

指针知识体系回顾

  • 数组名常退化为首元素指针——所有指针推导的起点
  • 「指针数组」存指针集合,「二级指针」是指针的指针
  • const 在 * 左/右决定保护的是数据还是地址
  • 函数指针数组把「调用哪个函数」变成数据驱动
  • 每多一层间接寻址就多一次解引用,需权衡性能
延伸主题:函数指针作为回调机制指针与引用的本质区别多级指针的合理边界
19第 19 页 · 指针知识体系回顾

核心概念自测

点击作答

指针数组 `char *names[3]` 作为函数实参传递时,形参最准确的声明是?

20第 20 页 · 核心概念自测

课后思考

先独立思考,再对照参考答案——答案点到为止,思路才是关键。

1为什么说指针数组是'二级指针的雏形'?它们在内存视角下究竟有什么本质联系?

参考答案指针数组如 int* arr[3] 在多数表达式求值时会退化为 int**,因此可整体赋给二级指针变量。它们指向一组指针,本质联系在此;但指针数组长度固定,二级指针更灵活。

2实际项目中,哪些情况必须把二级指针作为函数参数?请举一个具体场景。

参考答案典型场景:函数内要修改调用方的指针变量(如链表节点插入、动态二维数组分配、处理 main 的 argv)。核心思路:传指针的地址,让函数能改外层指针本身。

3char** pp 在语法上能像二维数组一样用 pp[i][j] 索引,但为什么说它是陷阱而非真正的二维数组?

参考答案真正的二维数组内存连续、行宽固定;char** 各行独立堆分配,行长可变,pp[i][j] 需两次解引用。索引语法掩盖了内存布局差异,混淆会导致段错误。

21第 21 页 · 课后思考