第 8 章:数组——程序员的储物神器
14 分钟阅读
第 8 章:数组——程序员的储物神器
各位老铁,上一章我们学习了变量和数据类型,那感觉就像学会了造单个砖块。但是!你见过只盖一层楼的房子吗?除非你是霍比特人,否则你肯定需要楼房。同样的,程序世界也需要一种"数据结构",能让你把一堆相关的变量打包存放管理。这就是我们今天的主角——数组(Array)!
打个形象的比喻:数组就像是一排整齐的储物柜,每个柜子都有编号,你可以往里面存东西,也可以取出来。每个柜子就是一个元素(Element),柜子的编号就是下标(Index)。
8.1 数组的定义与初始化
8.1.1 完全初始化 vs 部分初始化
在 C 语言中,定义数组就像去宜家买收纳柜——你得告诉程序:“我要买多大尺寸的柜子,里面放什么东西。”
完全初始化
| |
输出:
第 1 个人的成绩是: 90
第 2 个人的成绩是: 85
第 3 个人的成绩是: 77
第 4 个人的成绩是: 92
第 5 个人的成绩是: 88
这里 int scores[5] 意思是:“我要 5 个连续的整数存储空间”,然后 = {90, 85, 77, 92, 88} 意思是:“每个柜子里分别放这些分数”。
部分初始化
如果只给部分元素赋值,会发生什么?好奇宝宝们请看:
| |
输出:
numbers[0] = 10
numbers[1] = 20
numbers[2] = 30
numbers[3] = 0
numbers[4] = 0
未初始化的元素自动被填充为 0!这就像你只往柜子里放了前3个格子的东西,后面的格子自动变成了空的(0)。
这就是 C 语言的**零初始化(Zero Initialization)**特性——局部数组如果不主动赋值,未使用的元素自动归零。省心吧?
当然,如果你想清一色全是 0,可以更偷懒一点:
| |
8.1.2 自动计算大小
有时候你懒得数数组里到底有几个元素,比如:
| |
编译器会自动帮你数一数,发现有 7 个元素,于是 arr 的长度就是 7。这比自己数然后写 int arr[7] 优雅多了!
不过注意,这种偷懒写法只能在定义的同时初始化时使用。如果你是先声明后赋值,那就不行了:
1 2int arr[]; // 编译错误:数组定义时必须已知大小 arr = {1, 2, 3}; // 更不可能,数组名不是左值!
8.2 数组下标:arr[0]
好,现在我们知道怎么定义数组了,接下来是如何访问数组中的元素。秘诀就是——下标(Index)。
| |
输出:
arr[0] = 100
arr[1] = 200
arr[2] = 300
arr[3] = 400
arr[4] = 500
下标为什么从 0 开始?
这是让无数初学者挠头的问题!为什么不是 arr[1] 是第一个?
答案藏在指针偏移(Pointer Offset)的概念里。数组名 arr 实际上指向数组的首地址,也就是第一个元素的内存位置。
当你写 arr[0] 时,编译器内部把它转换成 *(arr + 0),就是 *arr——解引用首地址,得到第一个元素。
当你写 arr[1] 时,编译器内部把它转换成 *(arr + 1)——跳过一个 int 的大小,再解引用,得到第二个元素。
所以:
arr[0]等价于*(arr + 0)— 偏移 0 个位置arr[1]等价于*(arr + 1)— 偏移 1 个位置arr[2]等价于*(arr + 2)— 偏移 2 个位置
’/%3E%3Ctext x=‘250’ y=‘35’ text-anchor=‘middle’ fill=’%23333333’ font-family=‘Arial’ font-size=‘12’%3E偏移量: 0 1 2 3 4%3C/text%3E%3C/svg%3E)
想象一下,数组是一排连续的停车位,
arr是第一辆车停的那个位置的门牌号。arr[0]就是这个位置本身,arr[1]是往右一个位置,arr[2]是往右两个位置。如果从 1 开始编号,那每次访问arr[i]都要做arr[i-1]的转换——多麻烦!计算机最讨厌多做一步运算,所以干脆从 0 开始!
8.3 数组的内存布局:连续存储
数组在内存中是**连续存放(Contiguous Allocation)**的,就像一排紧紧挨着的联排别墅,没有任何缝隙。
假设我们有这样的代码:
| |
在内存中大概是这样的:
内存地址(假设从 0x1000 开始,int 占 4 字节):
0x1000 0x1001 0x1002 0x1003 | 0x1004 0x1005 0x1006 0x1007 | ...
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
┌─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐
│ arr[0] │ arr[1] │ arr[2] │ arr[3] │ arr[4] │ ??? │ ??? │
│ 10 │ 20 │ 30 │ 40 │ 50 │ │ │
└─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┘
4字节 4字节 4字节 4字节 4字节
每个 int 占 4 个字节(具体取决于系统),所以每个元素紧紧相连。
这种连续存储的好处是:访问速度超快! CPU 可以直接通过"首地址 + 偏移量 × 元素大小"计算出任何一个元素的位置,就像你知道一排柜子的第一个柜子在哪,然后轻松数到第 N 个柜子。随机访问的时间复杂度是 O(1),也就是常数时间!
8.4 数组名作为指针
这是一个超级重要又容易混淆的概念!让我们来好好掰扯掰扯。
&arr[0] 与 arr 的等价性
| |
输出:
arr 的值(首地址): 0x7ffd5a3b8c50
&arr[0] 的值: 0x7ffd5a3b8c50
两者相等吗? YES!
arr[0] = 1
*arr = 1
*(&arr[0]) = 1
所以:
arr的值 = 数组首元素的地址 =&arr[0]*arr= 访问 arr 指向的地址的内容 =arr[0]*(arr + i)=arr[i]
⚠️ 8.4.1 sizeof(arr) 为整个数组大小,不是指针大小
这是面试题和考试中的经典陷阱!请大家注意:
| |
输出(64 位系统):
sizeof(arr) = 20 字节 (整个数组: 5 × 4 = 20 字节)
sizeof(ptr) = 8 字节 (指针大小,在64位系统上是8字节)
sizeof(*ptr) = 4 字节 (一个 int 的大小)
关键区别:
sizeof(arr)中的arr是数组名,代表整个数组,所以返回的是整个数组的字节数(5 × 4 = 20)。而ptr只是一个指针变量,它的大小永远是 8 字节(64 位系统)。记住,arr在大多数情况下会"退化"为指针,但sizeof是个例外——它会保留数组的完整大小信息!
8.5 遍历数组
最常用的数组操作之一就是遍历——从头到尾访问每一个元素。
使用 for 循环
| |
输出:
=== 全班成绩 ===
第 1 个同学的成绩: 95
第 2 个同学的成绩: 87
第 3 个同学的成绩: 92
第 4 个同学的成绩: 78
第 5 个同学的成绩: 88
总分: 440,平均分: 88.0
使用指针遍历(进阶玩法)
| |
输出:
=== 用指针遍历数组 ===
arr[0] = 10(通过指针: *(p+0) = 10)
arr[1] = 20(通过指针: *(p+1) = 20)
arr[2] = 30(通过指针: *(p+2) = 30)
arr[3] = 40(通过指针: *(p+3) = 40)
arr[4] = 50(通过指针: *(p+4) = 50)
=== 指针递增遍历 ===
10 20 30 40 50
8.6 变长数组(VLA)
C99 引入的变长数组
传统 C 数组的大小必须是常量表达式(编译时就确定)。但 C99 引入了变长数组(Variable Length Array,简称 VLA),允许数组大小在运行时根据变量来确定:
| |
输入:
请输入要存储的成绩数量: 3
现在请输入 3 个成绩:
第 1 个成绩: 95
第 2 个成绩: 88
第 3 个成绩: 77
输出:
你输入的成绩是: 95 88 77
这就像你去快递站取包裹,包裹数量是到了才知道的(运行时确定),然后你临时要了对应数量的箱子来装。VLA 就是这样一个"临时工"——数组大小在程序运行时才决定。
C23 中 VLA 成为可选特性
不过要注意,C23 标准将 VLA 标记为可选特性(Optional Feature),也就是说:
- 在 C99、C11、C17 中,VLA 是标准特性
- 在 C23 及以后,编译器可以选择不支持 VLA
所以,如果你在面试中被问到 VLA,记得补充一句:“虽然 VLA 很方便,但因为性能和调试问题,C23 已经把它变成可选项了。实际工作中,如果需要动态大小的数组,更推荐使用
malloc动态分配内存。”
8.7 多维数组:二维数组、三维数组
现实世界往往比一维更复杂。比如:
- 班级里有多个学生,每个学生有多门成绩 → 二维数组
- 教学楼里有多个班级,每个班级有多个学生,每个学生有多门成绩 → 三维数组
二维数组的定义与初始化
| |
输出:
=== 成绩表 ===
学生 1: 95 87 92 78
学生 2: 88 91 85 90
学生 3: 77 82 89 95
三维数组的定义
| |
输出:
班级1-学生1: 90 85 88 92
班级1-学生2: 78 82 80 85
班级1-学生3: 95 91 89 93
班级2-学生1: 88 90 85 87
班级2-学生2: 92 89 91 88
班级2-学生3: 80 83 78 82
8.8 二维数组的内存布局(行优先存储)
虽然我们写代码时用 scores[3][4] 看起来像表格,但 C 语言在内存中实际是怎么存储的呢?
C 语言采用的是行优先存储(Row-Major Order)。简单来说就是:先填满第一行,再填第二行,再填第三行……
| |
在内存中实际布局:
逻辑视角(表格):
┌─────────┬─────────┬─────────┐
│ arr[0] │ arr[1] │ arr[2] │ ← 第一行
├─────────┼─────────┼─────────┤
│ arr[3] │ arr[4] │ arr[5] │ ← 第二行
└─────────┴─────────┴─────────┘
内存实际布局(行优先,一维连续):
内存地址: ... 0x00 0x01 0x02 | 0x03 0x04 0x05 ...
↓ ↓ ↓ ↓ ↓ ↓
┌────┬────┬────┬────┬────┬────┐
│ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │
└────┴────┴────┴────┴────┴────┘
↑ ↑
第一行 第二行
想象成一个停车场:
arr[2][3]就是 2 行 3 列的停车位。停车的时候,先把第一行的 3 个车位停满,再停第二行。内存里就是一维连续排列的 6 辆车。
下面这个程序验证了行优先存储:
| |
输出:
=== 二维数组各元素的内存地址 ===
&arr[0][0] = 0x7ffd5a3b8c50 → 值: 1
&arr[0][1] = 0x7ffd5a3b8c54 → 值: 2
&arr[0][2] = 0x7ffd5a3b8c58 → 值: 3
&arr[1][0] = 0x7ffd5a3b8c5c → 值: 4
&arr[1][1] = 0x7ffd5a3b8c60 → 值: 5
&arr[1][2] = 0x7ffd5a3b8c64 → 值: 6
地址从 0x...c50 到 0x...c64,每个 int 占 4 字节,可以清楚看到地址是连续递增的!
8.9 多维数组作为函数参数
把二维数组传给函数时,第二维必须指定。这是为什么呢?
| |
输出:
=== 第一种写法 ===
95 87 92 78
88 91 85 90
77 82 89 95
=== 第二种写法(指针+行数参数) ===
95 87 92 78
88 91 85 90
77 82 89 95
为什么第二维必须指定?因为编译器要知道每一行有多长,才能正确计算
scores[i][j]在内存中的位置。回顾一下行优先存储,scores[i][j]的地址 = 首地址 + i × (第二维大小) × 元素大小 + j × 元素大小。如果第二维大小未知,编译器就算不出偏移量,代码就没法工作。
等价的形式还有:
| |
8.10 常见错误
错误一:数组越界
这是 C 语言中最危险也最常见的错误之一!
| |
数组越界(Out of Bounds)就像你闯入了邻居的房子。编译器不会阻止你,程序可能看起来正常运行(读到了其他变量的值,或者刚好那片内存没人用),但更多时候会导致程序崩溃、数据损坏,甚至被黑客利用(缓冲区溢出攻击)。C 语言不检查数组越界,程序员自己要心里有数!
错误二:数组名作为左值
| |
数组名
arr在表达式中会退化(Decay)为指向首元素的指针,但这个指针是常量指针——你不能改变它的指向,也不能把它赋值给另一个指针变量(虽然指针本身的值可以赋给另一个指针)。就像一个房间的门牌号,你能用它找到房间,但你不能把门牌号改成另一个地址。
正确做法——用 memcpy 或循环复制:
| |
输出:
arr[0] = 10
arr[1] = 20
arr[2] = 30
arr[3] = 40
arr[4] = 50
错误三:sizeof 误用
| |
函数参数中的
arr[]实际上是一个指针!当数组作为函数参数传递时,它会退化为指针,丢失了数组的长度信息。所以sizeof(arr)在函数内部只能得到指针的大小(8 字节),而不是整个数组的大小。这就是为什么 C 数组传给函数时,必须额外传递一个长度参数的原因。
本章小结
数组是相同类型元素的连续存储结构,通过下标访问,下标从 0 开始(因为指针偏移)。
部分初始化的数组,未赋值的元素自动初始化为 0。
编译器可以自动计算数组大小:
int arr[] = {1, 2, 3}会自动创建一个长度为 3 的数组。数组名在表达式中退化为指针,但
sizeof(arr)保留数组完整大小。**变长数组(VLA)**在 C99 引入,C23 中成为可选特性。
二维数组按行优先存储,多维数组作为函数参数时,除了第一维可以省略,其他维度必须指定大小。
数组越界是未定义行为,可能导致程序崩溃或安全漏洞;数组名不能作为左值赋值;
sizeof在函数参数中会退化为指针大小而非数组大小。
数组是 C 语言中最基础也是最重要的数据结构。掌握了数组,你就掌握了程序世界中"批量处理数据"的基本功。下一章我们将学习字符串——一种用数组存放的特殊数据类型,敬请期待!