跳到主要内容
🔍
1.3.2已发布intermediate · 约 3 课时 · P0

指针与数组/字符串/多级指针

理清指针数组、数组指针与多级指针,掌握指针与数组/字符串互操作与 argv 参数解析

  • 来源
  • 补充

标记说明:【来源】来自上传资料 · 【补充】课程新编 · 【纠错】按勘误表修正 · 【更新】过时内容已现代化 · 【待确认】无法可靠还原

完成标准(本章)

  • 📖 已阅读:滚动 ≥ 80% 且有效阅读 ≥ 180
  • ✏️ 已练习:小练习正确率 ≥ 60%
  • 📝 已通过测验:分数 ≥ 60
  • 🛠️ 已掌握还需完成实践任务
学习状态:未开始

1.3.2 指针与数组/字符串/多级指针

本章来源:指针与一维/二维数组互操作、指针数组与数组指针、二级指针与指针传址、main 参数来自《第一阶段讲义》19.3/19.4/19.6 节【来源】,经重新组织表述;示例代码(原资料为截图无法还原)与全部练习为新编【补充】并本机实测(gcc 15.2.0)。平台适用性 universal。

① 学习目标

  1. 说清"数组到指针的转换"发生在哪些场合,以及数组名与指针变量的三点区别;
  2. 熟练用 p[i]*(p+i) 两种写法遍历数组,并说明指针运算只允许在同一数组对象内进行;
  3. 区分 int *p[3](指针数组)与 int (*p)[3](数组指针),能用数组指针遍历二维数组;
  4. 画出二级指针的指向关系图,说清"传址修改指针"需要二级指针(或返回指针);
  5. 实现 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++; 这个用法有什么问题?(单选)

    ◌ 未作答

⑨ 章节测验

章节测验

5 题题库 · 随机抽 5 题 · 及格线 60 分 · 前端判分(学习自测)
开始测验 →

⑩ 实战任务

实践任务

实现 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)
小练习 / 章节测验 / 实践任务 / 面试问题 / 延伸阅读补充原资料该章无成体系练习,全部新编