指针与数组/字符串/多级指针
理清指针数组、数组指针与多级指针,掌握指针与数组/字符串互操作与 argv 参数解析
- 来源
- 补充
标记说明:【来源】来自上传资料 · 【补充】课程新编 · 【纠错】按勘误表修正 · 【更新】过时内容已现代化 · 【待确认】无法可靠还原
完成标准(本章)
- 📖 已阅读:滚动 ≥ 80% 且有效阅读 ≥ 180 秒
- ✏️ 已练习:小练习正确率 ≥ 60%
- 📝 已通过测验:分数 ≥ 60 分
- 🛠️ 已掌握还需完成实践任务
1.3.2 指针与数组/字符串/多级指针
本章来源:指针与一维/二维数组互操作、指针数组与数组指针、二级指针与指针传址、main 参数来自《第一阶段讲义》19.3/19.4/19.6 节【来源】,经重新组织表述;示例代码(原资料为截图无法还原)与全部练习为新编【补充】并本机实测(gcc 15.2.0)。平台适用性 universal。
① 学习目标
- 说清"数组到指针的转换"发生在哪些场合,以及数组名与指针变量的三点区别;
- 熟练用
p[i]与*(p+i)两种写法遍历数组,并说明指针运算只允许在同一数组对象内进行; - 区分
int *p[3](指针数组)与int (*p)[3](数组指针),能用数组指针遍历二维数组; - 画出二级指针的指向关系图,说清"传址修改指针"需要二级指针(或返回指针);
- 实现
main(int argc, char **argv)参数解析(实践任务)。
② 前置知识
- 必选:1.3.1 指针基础(地址/解引用/const 三种写法);1.2.5 数组(一维/二维内存布局);1.2.6 字符串(
'\0'结尾与指针遍历)。
③ 核心概念【来源】
| 概念 | 说明 |
|---|---|
| 数组到指针的转换 | 表达式中的数组名(除 sizeof、& 少数场合外)自动转换为首元素地址;函数形参里的 int a[] 实际是 int *a(衰减) |
p[i] 与 *(p+i) | 完全等价:p[i] 定义为 *(p+i);对指针或数组名都成立(a[i] 即 *(a+i)) |
| 指针运算边界 | p+n 只在结果仍指向同一数组对象(或"尾后一个位置")内才合法;跨数组比较与运算、越界解引用都是未定义行为 |
| 数组指针 | int (*p)[3]:p 是"指向 int[3] 数组"的指针;p+i 每次跨过整个 3 个 int(sizeof(int[3]) 字节);适合遍历二维数组的每一行 |
| 指针数组 | int *p[3]:p 是数组,元素是 3 个指针;char *argv[] 即指针数组(命令行参数) |
int *p[3] vs int (*p)[3] | 读法:从内向外——(*p) 先与 * 结合 → p 是指针,指向 int[3] 数组;无括号时 p[3] 先结合 → p 是数组,元素是 int* |
| 二级指针 | int **pp:指向"指向 int 的指针"的指针;用途:① 函数内修改调用者的指针变量(传 &p);② 指针数组(char *argv[] 衰减为 char **argv) |
| 传址修改指针 | 想改调用者的 int *p,函数参数用 int **pp,调用传 &p——不是"C 引用传递",而是把指针的地址复制进形参,据此间接修改所指的指针变量 |
| main 参数 | int main(int argc, char **argv):argc 是参数个数(含程序名本身),argv 是参数字符串指针数组(argv[argc] == NULL) |
④ 通俗解释【补充】
- 数组名是"门牌号",指针变量是"记事本":门牌号不能改写(数组名不可赋值),记事本可以改抄别的门牌号;
a[i]就是"从门牌 a 出发,敲第 i 家"(*(a+i))。 - 数组指针是"一整排商铺的导购牌":
int (*p)[3]的 p 指着"3 连铺",p+1 直接跳到下一排——所以用它遍历二维数组一行一行走,步长自动正确。 - 指针数组是"一排记事本":
char *names[]每格抄一个字符串的门牌号——argv 就是一排这样的记事本,每个本子记着一个参数的门牌号。 - 二级指针是"记事本的记事本":你要改朋友的记事本(指针变量),就得拿着"他记事本存放位置"的地址(二级指针)才能改。
- 一句话区分:先问"谁是数组、谁是指针"——括号把
*和名字绑在一起的就是指针((*p)[3]),否则名字先和[]结合就是数组(*p[3])。
⑤ 示例代码
代码示例与验证记录
- examples/ex1-pointer-array.c指针与一维数组互操作(p[i] 与 *(p+i)、指针差、数组形参衰减)✓ 已实测(gcc 15.2.0 / MinGW-w64 x86_64 / Windows 11, 2026-08-15)编译:
gcc ex1-pointer-array.c -o ex1 -std=c11 -Wall -Wextra -Wpedantic适用环境:LinuxWindows(MinGW)展开预期输出(实测)
*p = 10, p[2] = 30, *(p+2) = 30 q - p = 3(元素个数,3) &a[1] == p+1 ? yes p[0]=10 p[1]=20 p[2]=30 p[3]=40 p[4]=50
差异说明:本机实测(0 警告);q-p 是指针差(同一数组内),结果为元素个数 3
完整源码见 /code 代码示例页
- examples/ex2-array-pointer-pointer-array.c数组指针遍历二维数组 + 指针数组存字符串(sizeof 验证)✓ 已实测(gcc 15.2.0 / MinGW-w64 x86_64 / Windows 11, 2026-08-15)编译:
gcc ex2-array-pointer-pointer-array.c -o ex2 -std=c11 -Wall -Wextra -Wpedantic适用环境:LinuxWindows(MinGW)展开预期输出(实测)
1 2 3 4 5 6 names[0] = linux names[1] = driver names[2] = bsp sizeof(names) = 24(3 个指针)
差异说明:本机实测(0 警告);sizeof(names)=24 说明 names 是 3 个 8 字节指针的数组(32 位系统为 12)
完整源码见 /code 代码示例页
- examples/ex3-argv.cmain(int argc, char **argv) 参数解析(含空格参数)✓ 已实测(gcc 15.2.0 / MinGW-w64 x86_64 / Windows 11, 2026-08-15)编译:
gcc ex3-argv.c -o ex3 -std=c11 -Wall -Wextra -Wpedantic适用环境:LinuxWindows(MinGW)展开预期输出(实测)
argc = 4 argv[0] = <程序完整路径>/ex3.exe argv[1] = alpha argv[2] = beta argv[3] = two words
差异说明:本机实测(0 警告);argv[0] 为程序完整路径(平台显示不同);引号参数作为单个 argv 元素(无引号)
完整源码见 /code 代码示例页
- examples/ex4-ub-notes.txt指针越界与跨数组运算是未定义行为(conceptual:静态讲解,不运行)概念讲解示例✓ 文档核对(非执行)· C11 标准 N1570 条文核对, 2026-08-15编译:
无(静态讲解文本;UB 代码不运行)适用环境:LinuxWindows(MinGW)展开文档核对记录
(无运行输出——本示例为静态讲解文本,见文件内容)
差异说明:内容按 C11 标准核对:J.2(数组下标越界)、6.5.6p8(指针加法必须在同一数组对象内或尾后一个位置)、 6.5.8p5(指向不同数组对象的指针比较未定义)。不运行越界代码、不把某次输出写成语言保证
完整源码见 /code 代码示例页
⑥ 编译与运行方法
gcc ex1-pointer-array.c -o ex1 -std=c11 -Wall -Wextra -Wpedantic
./ex1 (Windows: ex1.exe)三个示例均为本机实测(gcc 15.2.0,0 警告),输出见示例卡折叠区;ex3 的 argv 输出在命令行加参数运行(./ex3 alpha beta "two words"——引号保证空格参数是一个整体)。
⑦ 常见错误
| 症状 | 原因 | 解决 |
|---|---|---|
对数组名赋值(a = p;) | 数组名是"首元素地址"的常量,不可作左值 | 用指针变量保存地址;数组内容用下标/解引用改 |
int *p[3] 与 int (*p)[3] 混用 | 括号决定结合顺序 | 从内向外读:(*p) 结合 → 指针指向 int[3];p[3] 结合 → 元素为 int* 的数组 |
对数组指针 p+1 步长理解错 | 步长是"整个所指数组"的字节数 | 用 sizeof(*p) 验证(指向 int[3] 时为 12) |
| 跨数组指针比较/运算、越界解引用 | 未定义行为 | 指针运算限定同一数组对象内(或尾后一个位置);遍历条件 p < a + n |
| 想修改调用者的指针却只传指针 | 形参是副本,改不了调用者的指针变量 | 传指针的地址(二级指针 int **pp),或返回新指针 |
| 把"传址"说成"引用传递" | C 只有传值 | 正确表述:把地址值复制进指针形参,间接修改所指对象 |
⑧ 小练习
小练习
学习自测:提交后才显示答案与解析(前端判分,不作为正式考试)ex-1-3-2-1.int (*p)[3] 与 int *p[3] 的正确读法分别是?(单选)
◌ 未作答ex-1-3-2-2.函数要修改调用者的 int *p 变量(让它指向别处),参数类型应该是?(单选)
◌ 未作答ex-1-3-2-3.代码审查:int grid[2][3]; int *p = grid; p++; 这个用法有什么问题?(单选)
◌ 未作答
⑨ 章节测验
章节测验
⑩ 实战任务
实践任务
实现 main(int argc, char **argv) 参数解析程序
实现一个命令行工具 mini-echo:打印参数个数与每个参数(含 argv[0]),支持 -u 选项把 后续参数转大写输出、-l 转小写(只处理 ASCII 字母);同时画出二级指针 argv 的指向关系图, 并写出 int *p[3] 与 int (*p)[3] 各自的定义与一句话用途。
输入与输出
程序输入:命令行参数(如 ./mini-echo -u hello "two words")。输出:argc 值、逐条 argv、 按选项转换后的输出(-u → HELLO、TWO WORDS;-l → hello、two words;无选项原样输出)。
功能要求
- 使用 int main(int argc, char **argv) 标准写法
- 遍历 argv 打印全部参数(练习指针数组遍历)
- 解析第一个参数为 -u / -l 选项,对其余参数做 ASCII 大小写转换(自写 to_upper/to_lower,不调 ctype 也可)
- 用 argv[1] 判断选项时先检查 argc >= 2(边界)
- 画出 argv 二级指针指向关系图(程序名与各参数字符串)
- 写出 int *p[3] 与 int (*p)[3] 的定义与一句话用途
限制条件
- 只处理 ASCII 字母的大小写,不要求 locale/多字节
- 不使用"引用传递"等不准确表述(C 只有传值)
验收步骤(自检清单 0/4)
验收标准
- 四种调用输出正确且 argc<2 时不会读 argv[1](验收步骤 2)
- 关系图体现 argv → argv[0..n] → 字符串(验收步骤 3)
- 对照说明类型判定正确(验收步骤 4)
常见失败原因
- 忘了 argc 边界直接读 argv[1](无参数时越界)
- 把 argv 说成 char* 而不是 char**
- 转换选项时把 argv[0](程序名)也转换了
可选扩展
- 支持 --help 选项打印用法并返回 0
- 统计并打印参数总字符数(strlen 累加)
完成必要清单后才能计入"已完成实践"(学习状态自动推导,不提供一键完成)
⑪ 面试问题
面试问题
int *p[3] 和 int (*p)[3] 有什么区别?各自用在什么场景?高频C/C++ · medium
要点:前者是"指针数组"(3 个 int* 元素);后者是"数组指针"(指向 int[3] 的一个指针)。前者用于存储多个字符串/对象地址(如 argv);后者用于以行遍历二维数组。
结合优先级决定类型:`int *p[3]` 中 `p[3]` 先结合 → p 是数组,元素为 int*(常用于字符串数组、指针表); `int (*p)[3]` 中 `(*p)` 先结合 → p 是指针,指向"3 个 int 的数组",p+1 步长为 sizeof(int[3]), 正好一行——因此二维数组 int a[N][3] 可以直接赋给 int (*p)[3] 逐行遍历。 判别口诀不背也罢,把握"从内向外读类型 + 步长验证"(sizeof(*p))。
追问:- 追问:二维数组 int a[2][3] 的数组名 a 是什么类型?(int (*)[3],即数组指针)
评分要点:- 两种类型准确定义
- 应用场景各举一例
函数里想修改调用者传入的指针本身(例如重新分配内存),应该怎么做?为什么只传指针不行?高频C/C++ · medium
要点:参数用二级指针(如 void init(int **pp)),调用传 &p;因为 C 参数一律传值,传指针只是复制了地址值,改不了调用者的指针变量本身。
C 只有传值:形参是实参的副本。传 int *p 时,函数内 p = 新地址 只是改副本, 调用者变量不变(常见于"在函数里 malloc 后返回给调用者"失败场景)。 正确做法:参数 int **pp,函数内 *pp = 新地址(解引用一级得到调用者的指针变量本身), 调用传 &p。或者返回新指针让调用者赋值。注意表述:这不是"引用传递", 而是"把指针的地址复制进形参,间接修改所指的指针变量"。
追问:- 追问:用返回值方案和二级指针方案各有什么取舍?(返回值单一但直观;二级指针可同时返回状态码)
评分要点:- 传值本质讲清
- 二级指针方案与返回值替代方案
数组名和指针变量有什么区别?哪些场合数组名不会退化为指针?高频C/C++ · medium
要点:数组名是"数组对象"的名字(首元素地址的常量,不可赋值、sizeof 得到整个数组大小);指针是变量。sizeof(a)、&a、字符串字面量初始化等场合不衰减。
三点区别:① 数组名不可作左值(不能 a = 新地址),指针变量可以;② sizeof(数组名) 是整个数组 字节数,sizeof(指针) 是指针本身大小;③ &a 的类型是"指向整个数组的指针"(int (*)[N]), 数值上与 &a[0] 相同但类型不同。例外场合:sizeof(a)、&a、_Alignof、字符串字面量初始化 char s[]="..." 等处不发生转换;函数形参里 int a[] 等价 int *a(衰减不可避免)。
追问:- 追问:&a 与 &a[0] 数值相同,那写 &a+1 会发生什么?(步长是整个数组)
评分要点:- 不可赋值/sizeof/& 三区别
- 不衰减的例外场合
⑫ 延伸阅读
- 《C 程序设计语言(K&R)》5.3/5.4/5.10 节——指针数组、命令行参数;
- 《C 语言程序设计:现代方法》第 12/13 章;
- 力扣交叉引用:LC 344(反转字符串)与 LC 283(移动零)的主要教学归属是 1.3.1;LC 1/26/121 归属 1.2.5;LC 125/14 归属 1.2.6——本章以 LC 27/88/167 三题练指针与数组互操作(见下方卡片,reference_only);
- 下一章预告:1.3.3 函数与递归——指针传参与递归调用栈的完整图景。
力扣推荐题
外部题目为「引用与推荐」模式(reference_only):仅保留题号、题名、难度与推荐理由,完整题面与提交请前往力扣;完成状态为自报记录,未与力扣账户验证,不影响章节掌握度判定。
- #27 移除元素Remove Element推荐练习(适配度 80)力扣难度:easy课程难度:L2预计 30 分钟章节必做前置:无推荐理由:
- 双指针/双下标原地改写,正是本章指针与数组互操作主题
- 无需其他越级前置
匹配知识点:pointerarray - #88 合并两个有序数组Merge Sorted Array推荐练习(适配度 75)力扣难度:easy课程难度:L2预计 30 分钟前置:无推荐理由:
- 双指针从后往前,数组+下标指针操作
- 无越级前置
匹配知识点:pointerarray - #167 两数之和 II - 输入有序数组Two Sum II - Input Array Is Sorted可选拓展(适配度 70)力扣难度:medium课程难度:L2预计 30 分钟前置:无推荐理由:
- 相向双指针,指针与数组综合
- 无越级前置
匹配知识点:pointerarray
迁移训练(migration training)
把本章技能迁移到 Linux 与 嵌入式:
| 环节 | Windows(MinGW) | Linux / 嵌入式 |
|---|---|---|
| 指针语义 | 一致 | 一致 |
| 指针大小 | 本机 8 字节(x86_64) | 64 位同;32 位/常见嵌入式为 4 字节 |
| argv | 一致(含程序名 argv[0]) | 一致 |
| 差异 | 路径分隔符影响 argv 内容形式 | 一致 |
不变的:指针运算边界、数组指针步长、二级指针指向关系、argv 解析逻辑;要改的:依赖 sizeof(指针)==8 的代码(用 sizeof(int*) 或 sizeof(void*) 而非写死数字)。
内容来源映射
| 内容部分 | 资料 | 位置 | 标记 | 说明 |
|---|---|---|---|---|
| 指针与一维/二维数组互操作、指针数组与数组指针、二级指针与指针传址、main 参数 | 第一阶段讲义 | 19.3/19.4/19.6 节 | 【来源】 | 正文在原资料基础上重新组织表述,未大段复制原文 |
| 示例代码(3 个可运行程序)与越界未定义行为讲解 | 无 | 【补充】 | 原资料示例代码以截图嵌入无法还原;本章示例全部新编并本机实测(gcc 15.2.0) | |
| 小练习 / 章节测验 / 实践任务 / 面试问题 / 延伸阅读 | 无 | 【补充】 | 原资料该章无成体系练习,全部新编 |