指针数组
看完你能画出指针数组的内存图,并解释它如何存一组字符串
按 空格/→ 演示下一步
全部页面点击任意一页,跳回舞台从这页播放
指针数组
看完你能画出指针数组的内存图,并解释它如何存一组字符串
普通数组的局限
上页把指针装进数组,让"装地址"成了可能。但反观普通数组本身,它有三个硬伤锁死了灵活性。
车位大小一致、必须排成一排、总数在建造时就定死
场景引入:字符串数组
上一页我们看到普通二维数组存字符串,每个短串都得按最长串对齐,浪费大量空间。那有没有办法让每个字符串「刚好」占自己需要的空间?这就要用到字符串数组。
书架上书签连续排列=指针数组;书本可放任意位置=字符串分散存储
普通数组 vs 指针数组
两者都能装多个字符串,但底层内存模型完全不同——这决定了能不能改。
- 一整块连续内存,编译期确定大小
- 按最大长度 M 分配,短串浪费空间
- 元素可改写,在栈或全局区
- N 个指针分散指向各字符串
- 各串按实际长度分配,零浪费
- 字面量只读,改写触发段错误
指针数组的定义
前页把字符串首地址交给数组后,问题落到“这些地址也能批量存放吗”?可以:先划出 5 个连续元素,再规定每个元素都是 int 指针。
五个格口对应 5 个元素,格内编码对应所存 int 地址
指针数组的内存布局
指针数组本身连续存指针,每个指针再指向各自的字符串。两层结构:数组一层、字符串一层。
指针数组的声明与初始化
用 sizeof 直接看穿 names[] 每个元素其实是个 8 字节的指针。
声明里 * 紧贴名字、[] 在后,说明 4 个元素都是指针;sizeof(names[0])=8 而非字符串长度,印证元素是指针;%s 通过解引用取出字符串。
通过指针数组访问元素
同一组 names,两套语法都能拿到字符串首地址——关键在于 names 本身的类型。
L11 点明下标与指针的等价;L14 用 names[i] 取第 i 个元素;L19 注意 p 是二级指针,类型必须与 names 元素匹配;L21 用 *p 解引用一次得到字符串首地址。
字符串数组:const char *
一段可运行的 C 程序:声明 const char* 指针数组,并打印每个元素的指针地址、字符串与首字符。
指针数组存的是字符串地址,不是字符串本体。%s 从指针读到 \0 终止,*names[i] 解引用拿到首字符——const char* 字符串数组的访问方式即由此展开。
字符串数组的内存模型
用代码把字符串数组的两层结构打出来:数组元素连续,指针指向只读段。
声明的数组在栈区连续,元素存的只是地址;这些地址指向 .rodata 里的字符串字面量,所以指针数组是「两级跳转」:先找指针,再找字面量。
函数指针数组
前页的指针数组把变量的地址排成一行;函数也有可调用的入口地址,因此可以把若干函数地址放进同一数组,按索引取得并调用目标函数,这正是函数指针数组的核心用途。
每个编号对应一个函数入口地址,按编号即可调用对应处理函数
函数指针数组示例
用查表代替 if/else 链:把同签名的运算函数按顺序塞进数组,输入字符索引过去直接调用。
ops[i] 把字符索引翻译成函数调用——这就是分支 dispatch:让数据(运算符)驱动代码(函数)的选择,而不是一连串 if/else。
二级指针的定义
指针数组名 int *arr[3] 传给函数时,参数类型其实是 int **——比指针数组再多一层 *,专门用来保存'指针的地址'。
一级指针是写着房号的纸条;二级指针指向装纸条的那个盒子
二级指针的内存模型
从上往下读:实线是『存了什么门牌号』,虚线是『解引用后等价于什么』。
二级指针的使用场景
用 int ** 在堆上搭出行列都可变的二维数组。
malloc 两层得到指针数组+数据块;matrix[i][j] 等价于 *(*(matrix+i)+j),体现「两次跳转」;free 必须反向,先行后表。
二级指针作为函数参数
通过 &ptr 传入指针自身的地址,函数里就能改写调用方的指针变量本身。
L5、L6 是核心:用 *p 改写调用方指针本身,再用 **p 写入数据。L12 传 &ptr,把指针自己的地址交给函数。
指针数组即二级指针的雏形
图解两者在内存中的等价关系
指针数组与二级指针对比
长得都像 `int**`,但数组是容器的视角,指针是地址的视角。
- 本质是数组:`int *arr[5]` 有 5 个元素
- 元素在内存中连续存放,按下标访问
- 适合字符串表、函数指针表等查表场景
- 本质是指针:`int **p` 只有 1 个变量
- 指向另一个 int*,多一层间接寻址
- 用于函数内修改外部指针、动态二维数组
指针知识体系回顾
- ✓数组名常退化为首元素指针——所有指针推导的起点
- ✓「指针数组」存指针集合,「二级指针」是指针的指针
- ✓const 在 * 左/右决定保护的是数据还是地址
- ✓函数指针数组把「调用哪个函数」变成数据驱动
- ✓每多一层间接寻址就多一次解引用,需权衡性能
核心概念自测
指针数组 `char *names[3]` 作为函数实参传递时,形参最准确的声明是?
课后思考
先独立思考,再对照参考答案——答案点到为止,思路才是关键。
参考答案指针数组如 int* arr[3] 在多数表达式求值时会退化为 int**,因此可整体赋给二级指针变量。它们指向一组指针,本质联系在此;但指针数组长度固定,二级指针更灵活。
参考答案典型场景:函数内要修改调用方的指针变量(如链表节点插入、动态二维数组分配、处理 main 的 argv)。核心思路:传指针的地址,让函数能改外层指针本身。
参考答案真正的二维数组内存连续、行宽固定;char** 各行独立堆分配,行长可变,pp[i][j] 需两次解引用。索引语法掩盖了内存布局差异,混淆会导致段错误。