数组
掌握一维/二维数组、初始化遍历与排序,理解数组名的地址本质
- 来源
- 补充
标记说明:【来源】来自上传资料 · 【补充】课程新编 · 【纠错】按勘误表修正 · 【更新】过时内容已现代化 · 【待确认】无法可靠还原
完成标准(本章)
- 📖 已阅读:滚动 ≥ 80% 且有效阅读 ≥ 180 秒
- ✏️ 已练习:小练习正确率 ≥ 60%
- 📝 已通过测验:分数 ≥ 60 分
- 🛠️ 已掌握还需完成实践任务
1.2.5 数组
本章来源:一维/二维数组定义、初始化、遍历与存储方式来自《第一阶段讲义》17.x 节【来源】,经重新组织表述;三个可运行示例、越界 UB 的概念讲解与全部练习为新编【补充】并本机实测。平台适用性 universal。本章按既定映射加入力扣参考题(reference_only,不复制题面)。
① 学习目标
- 完成一维数组的定义、初始化与遍历,说清"部分初始化补零"与"长度推导";
- 说出数组越界是未定义行为(UB),而不是"一定崩溃",并能审查循环边界;
- 说清"数组名 = 首元素地址(常量)":可取值、不可赋值;
- 知道数组作函数参数时退化为指针(结论先行,机制在 1.3.1 展开);
- 画出二维数组的行优先内存布局(相邻元素地址连续);
- 手写冒泡/选择/插入三种排序,并说出三者交换/移动代价的差别。
② 前置知识
- 必选:1.2.4 控制结构(for 循环遍历与边界条件 i < n)。
③ 核心概念【来源】
| 概念 | 说明 | 要点 |
|---|---|---|
| 定义 | 类型 名字[长度],长度必须是常量表达式(C99 起变长数组 VLA 为可选特性,教学建议用常量) | 长度在定义时确定,之后不能改变 |
| 初始化 | int a[5] = {10, 20, 30} | 未列出的元素自动补 0;int b[] = {1,2,3} 长度推导为 3 |
| 越界 = UB | 下标超出 0..n-1 是未定义行为(C11 J.2、6.5.6) | 可能读到垃圾、可能恰好正常、可能崩溃——标准不保证任何结果,详见 ex4 概念讲解 |
| 数组名 = 首元素地址 | a 与 &a[0] 相等(ex1 实测) | 数组名是常量:不能 a = 别的数组;sizeof(a) 是整个数组大小(不是 4/8) |
| 函数参数退化 | 数组作函数形参退化为指针,函数内 sizeof(参数) 是指针大小 | 结论先行:需要长度时把 n 一起传入;机制 1.3.1 展开 |
| 二维数组 | int m[2][3] 在内存中行优先连续存放 | ex2 实测:&m[0][0]、&m[0][1]、&m[0][2]、&m[1][0] 依次相差 sizeof(int) |
| 字符数组预告 | char s[] = "hi" 实际长度 3(含结尾 '\0') | 1.2.6 字符串完整展开 |
④ 通俗解释【补充】
- 数组是一排储物柜:每个格子有门牌号(下标),从 0 开始数——第 1 格是 0 号,第 n 格是 n-1 号;
- 越界是伸手到墙外:柜子只有 n 格,你开 n 号门——门后面是什么?可能是隔壁房间(读垃圾)、可能是墙(崩溃)、也可能凑巧有东西(看似正常)。因为它"可能正常",所以越界 bug 特别阴险——这也是为什么不靠"跑一遍看结果"来学它(ex4);
- 数组名是门牌:它告诉你"这排柜子从哪开始",但它是一块钉死的铭牌——你可以读它,不能把它拆下来贴到别处(常量);
- 把数组交给函数:你只把"第一格在哪"抄给函数(指针),函数并不知道一共几格——所以长度 n 也要一并告诉它;
- 二维数组是长条柜:所谓 2 行 3 列,内存里其实是一字排开的 6 格,按"第一行 3 格、再第二行 3 格"的顺序放(行优先)。
⑤ 示例代码
代码示例与验证记录
- examples/ex1-array-basics.c一维数组:部分初始化补零 / 长度推导 / 数组名即首元素地址✓ 已实测(gcc 15.2.0 / MinGW-w64 x86_64 / Windows 11, 2026-08-16)编译:
gcc ex1-array-basics.c -o ex1 -std=c11 -Wall -Wextra -Wpedantic适用环境:LinuxWindows(MinGW)macOS展开预期输出(实测)
部分初始化(声明长度 5,只给 3 个值): a[0] = 10 a[1] = 20 a[2] = 30 a[3] = 0 a[4] = 0 长度推导: sizeof(b)/sizeof(b[0]) = 3 数组名是首元素地址: a = 0000007F1B1FFC00, &a[0] = 0000007F1B1FFC00
差异说明:编译 0 警告;a[3]/a[4] 的 0 是"未列出元素自动补零"的实测证据;地址值每次运行不同,关键是 a 与 &a[0] 相等
完整源码见 /code 代码示例页
- examples/ex2-2d-array.c二维数组行优先布局(打印每个元素的地址)✓ 已实测(gcc 15.2.0 / MinGW-w64 x86_64 / Windows 11, 2026-08-16)编译:
gcc ex2-2d-array.c -o ex2 -std=c11 -Wall -Wextra -Wpedantic适用环境:LinuxWindows(MinGW)macOS展开预期输出(实测)
m[0][0] = 1 @ 000000C5F4BFF670 m[0][1] = 2 @ 000000C5F4BFF674 m[0][2] = 3 @ 000000C5F4BFF678 m[1][0] = 4 @ 000000C5F4BFF67C m[1][1] = 5 @ 000000C5F4BFF680 m[1][2] = 6 @ 000000C5F4BFF684
差异说明:编译 0 警告;地址值每次运行不同——看**相邻差值**:依次 +4(本机 int 4 字节),证明"行优先连续存放"(跨行 m[0][2]→m[1][0] 也连续)
完整源码见 /code 代码示例页
- examples/ex3-sorts.c冒泡/选择/插入三种排序与交换/移动代价统计✓ 已实测(gcc 15.2.0 / MinGW-w64 x86_64 / Windows 11, 2026-08-16)编译:
gcc ex3-sorts.c -o ex3 -std=c11 -Wall -Wextra -Wpedantic适用环境:LinuxWindows(MinGW)macOS展开预期输出(实测)
原始数据: 5 2 8 1 9 冒泡排序: 1 2 5 8 9 相邻交换次数: 4 选择排序: 1 2 5 8 9 交换次数: 2 插入排序: 1 2 5 8 9 元素后移次数: 4
差异说明:编译 0 警告;对 {5,2,8,1,9} 的本机真实统计(换数据数值会变,观察机制:选择排序每轮至多交换一次所以次数最少;复杂度分析在 2.1.1)
完整源码见 /code 代码示例页
- examples/ex4-ub-notes.txt数组越界 = 未定义行为(概念讲解,不运行越界代码)概念讲解示例✓ 文档核对(非执行)· C11 标准 ISO/IEC 9899:2011 J.2 / 6.5.6 核对, 2026-08-16编译:
无(conceptual 讲解,不编译不运行)适用环境:LinuxWindows(MinGW)macOS展开文档核对记录
# 数组越界:未定义行为(C11 标准依据 J.2 与 6.5.6) # 未定义行为的三种可能表现:读到垃圾值 / 恰好看似正常 / 崩溃 # 安全公式:n 个元素合法下标 0..n-1,遍历条件 i < n # 防御预告:-fsanitize=address(5.6 展开)
差异说明:依据 C11 标准(ISO/IEC 9899:2011)J.2 未定义行为清单与 6.5.6 指针运算规则核对;纪律:越界是 UB,不做"跑一次总结规律"式演示
完整源码见 /code 代码示例页
⑥ 编译与运行方法
- ex1/ex2/ex3 均为单文件程序,编译命令统一
-std=c11 -Wall -Wextra -Wpedantic(见示例卡); - ex2 打印的地址值每次运行不同,学习时看相邻地址的差值(本机 int 为 4 字节:依次 +4);
- ex3 的交换/移动次数是本机对
{5,2,8,1,9}的真实统计(冒泡交换 4 次、选择交换 2 次、插入后移 4 次)——换一组数据数值会变,观察的是"数量级与机制"; - ex4 是概念讲解文本,不编译不运行(越界是 UB,跑一次得不到可依赖规律)。
⑦ 常见错误
| 症状 | 原因 | 解决 |
|---|---|---|
| 输出莫名其妙的数 / 偶发崩溃 | 循环条件 i <= n 越界访问 a[n] | 条件改 i < n;用边界代入法审查(1.2.4 学过) |
| 编译报"数组长度不是常量" | 用了变量定义长度(C99 VLA 是可选特性) | 教学用常量或 #define;确需运行时长度用动态内存(1.3.9) |
a = b; 编译报错 | 数组名是常量地址,不能整体赋值 | 逐元素拷贝(for 循环)或 memcpy(需 string.h,1.2.6 介绍) |
函数里 sizeof(arr) 是 8 而不是数组大小 | 参数退化为指针 | 把长度 n 作为参数传入;这是面试高频考点 |
| 排序后原数组顺序也变了 | 排序函数直接改传入数组(原地排序) | 需要保留原数据时先拷贝一份再排(ex3 的做法) |
| 二维数组初始化结果"错位" | 内层花括号省略导致编译器按行优先自动分配 | 建议每行用一对花括号 {{1,2,3},{4,5,6}} 写清楚 |
⑧ 小练习
小练习
学习自测:提交后才显示答案与解析(前端判分,不作为正式考试)ex-1-2-5-1.关于数组名 a(int a[10]),下面哪句正确?(单选)
◌ 未作答ex-1-2-5-2.求 int arr[6] = {4, 9, 2, 7, 1, 5} 的最大值,循环体里正确的比较是?(单选)
◌ 未作答ex-1-2-5-3.遍历 int a[5] 时把循环写成 for (i = 0; i <= 5; i++),后果是?(单选)
◌ 未作答
⑨ 章节测验
章节测验
⑩ 实战任务
实践任务
手写三种排序并比较 + 画出二维数组内存布局图
不照抄示例,独立手写冒泡、选择、插入三种排序(对同一组数据分别统计交换/移动次数), 并手绘一张 int m[2][3] 的行优先内存布局图(标注地址偏移)。
输入与输出
无固定输入输出。交付物:① 一个包含三种排序的 .c 文件(或三个独立文件);② 每种排序对 同一组数据的输出与代价统计;③ 二维数组内存布局图(含相邻元素偏移与下标公式)。
功能要求
- 手写三种排序(可参考 ex3 思路,不得整段照抄),数据至少 8 个元素
- 每个排序函数统计并输出交换(或移动)次数
- 编译 0 警告(-std=c11 -Wall -Wextra -Wpedantic)并运行输出正确
- 画出 m[2][3] 行优先布局图,标注 m[1][2] 的偏移公式(1*3+2)
限制条件
- 不使用 qsort 等库排序函数(手写是本章目标)
- 不运行任何越界访问代码(越界是 UB,只做概念理解)
验收步骤(自检清单 0/4)
验收标准
- 三种排序对同一组数据输出一致且升序正确(验收步骤 2)
- 代价统计与排序机制相符(选择排序交换次数最少)(验收步骤 3)
- 布局图行优先顺序正确、偏移公式正确(验收步骤 4)
常见失败原因
- 排序函数忘记把结果写回数组(用了局部拷贝)
- 内层循环边界写成 j < n 而非 j < n-1-i(冒泡)导致越界
- 交换时漏了临时变量(两个赋值互相覆盖)
- 选择排序每次比较都交换(应每轮最多交换一次)
可选扩展
- 给排序加稳定性验证:对含相等元素的记录(如 {3,1,3,2})观察选择排序是否改变相等元素顺序
- 统计 1..100 内的质数个数(答案 25 个),并用数组保存这些质数(衔接 1.2.4 与本章)
完成必要清单后才能计入"已完成实践"(学习状态自动推导,不提供一键完成)
⑪ 面试问题
面试问题
数组名和指针有什么区别?sizeof 在两种情况下各得到什么?高频C/C++ · medium
要点:数组名是首元素地址的常量表达式(不可赋值);指针是变量(可改指向)。sizeof(数组名) 是整个数组大小,sizeof(指针) 是指针本身大小。
数组名在表达式中多数场合退化为首元素地址,但它本身是常量——不能 arr = 别的地址; 指针是普通变量,可以重新赋值。sizeof 是二者最直观的分水岭:sizeof(arr) 是整个数组 字节数(如 int a[10] 为 40),sizeof(ptr) 是指针大小(本机 8)。另外数组名作为函数 参数时退化为指针,函数内 sizeof 得到的也是指针大小——这是必须把长度 n 一并传入的原因。
追问:- 追问:&arr 与 arr 有什么区别?(值相同、类型不同:&arr 是指向整个数组的指针,+1 步长为整个数组)
评分要点:- 常量 vs 变量
- sizeof 差异
- 参数退化与长度传递
数组越界访问会发生什么?为什么说"测试通过"不代表没有越界?高频C/C++ · medium
要点:越界是未定义行为(UB):可能读到垃圾、可能崩溃、可能看似正常;不同编译器/优化级别/环境结果不同,所以一次测试通过不能证明安全。
C11 标准(J.2、6.5.6)把下标越界规定为未定义行为——标准不保证任何结果。实际中: 读到相邻内存可能"恰好是 0",也可能破坏相邻变量造成后续奇怪 bug,可能触发段错误。 正因为"看似正常"是合法表现之一,靠运行测试无法排除越界;正确姿势是边界审查 (0..n-1、i < n)+ 工具辅助(AddressSanitizer 在 5.6 展开)。面试加分:主动提出 用 ASan/静态检查而不是"多跑几遍"。
追问:- 追问:如何用工具暴露越界?(-fsanitize=address 编译运行)
评分要点:- UB 定义准确
- 三种可能表现
- 测试不能证明安全
二维数组在内存中如何存放?m[1][2] 在 m[0][0] 之后的第几个位置?C/C++ · medium
要点:C 数组行优先:按行连续存放;对 int m[2][3],m[1][2] 是第 6 个元素(下标 5),地址 = 首地址 + 5*sizeof(int)。
行优先:第一行的 3 个元素先连续放,再放第二行的 3 个(ex2 实测相邻地址依次 +4)。 寻址公式:m[i][j] 相对首地址偏移 (i * 列数 + j) * sizeof(元素)。所以 m[1][2] 是第 6 个元素(偏移 5 个 int)。行优先是 C 的硬性规定,与 1.3.2 的指针算术 (m[i] 即第 i 行首地址)完全一致。加分:能解释为什么必须指定列数(类型 int[3] 才能算出一行步长)。
追问:- 追问:为什么二维数组作参数时必须给出列数(int m[][3])?
评分要点:- 行优先与偏移公式
- 与指针算术一致性
- 列数必需的原因
⑫ 延伸阅读
- 《C 程序设计语言(K&R)》1.6 节与第 5 章——只引书名与章节名;
- 《C 语言程序设计:现代方法》第 8 章 数组;
- 力扣交叉引用:LC 283(移动零)的主要教学归属是 1.3.1(已完成),本章以 LC 1/26/121 三题练数组基本操作(见下方力扣卡片,reference_only);
- 下一章预告:1.2.6 字符串与安全函数——字符数组 + 结尾
'\0'的完整约定。
力扣推荐题
外部题目为「引用与推荐」模式(reference_only):仅保留题号、题名、难度与推荐理由,完整题面与提交请前往力扣;完成状态为自报记录,未与力扣账户验证,不影响章节掌握度判定。
- #1 两数之和Two Sum推荐练习(适配度 85)力扣难度:easy课程难度:L1预计 30 分钟章节必做前置:无推荐理由:
- 仅需一维数组与双重循环,无需指针/函数进阶
- 数组遍历+下标检索是本章核心技能
- 题目规模小、无越级前置
匹配知识点:array - #26 删除有序数组中的重复项Remove Duplicates from Sorted Array可选拓展(适配度 70)力扣难度:easy课程难度:L2预计 30 分钟前置:无推荐理由:
- 双下标原地改写,无需指针变量(用整型下标即可)
- 适合学完本章练习
匹配知识点:array - #121 买卖股票的最佳时机Best Time to Buy and Sell Stock可选拓展(适配度 65)力扣难度:easy课程难度:L2预计 30 分钟前置:无推荐理由:
- 单循环+最小值变量,数组基本操作
- 理解上无越级前置
匹配知识点:array
迁移训练(migration training)
数组语义与内存布局规则三平台一致;嵌入式两个注意点:
- 栈空间有限:任务栈通常只有几 KB,
int buf[1 << 20]这类大数组应改为 static 或堆内存(1.3.9 动态内存展开); - 平台相关只体现在元素大小:sizeof(int) 可能为 2/4(本机 4),涉及字节布局时用
sizeof而不是写死数字(ex2 的地址差值演示同理)。
内容来源映射
| 内容部分 | 资料 | 位置 | 标记 | 说明 |
|---|---|---|---|---|
| 一维/二维数组定义、初始化、遍历、存储方式与数组名含义 | 第一阶段讲义 | 17.x 节 | 【来源】 | 正文在原资料基础上重新组织表述,未大段复制原文 |
| 三个可运行示例(初始化补零/行优先地址/三种排序代价统计)与本机实测输出 | 无 | 【补充】 | 原资料无成体系可运行示例,全部新写并本机实测(gcc 15.2.0) | |
| 越界未定义行为的 conceptual 讲解(ex4,引 C11 标准 J.2/6.5.6) | 无 | 【补充】 | 纪律:越界是 UB,只做静态讲解、不运行越界代码总结规律 | |
| 小练习 / 章节测验 / 实践任务 / 面试问题 / 延伸阅读 | 无 | 【补充】 | 原资料该章无成体系练习,全部新编 |