第1章:数组的存储与访问
数组这东西,C语言里最基础的数据结构之一。但说实话,很多人用了好几年,对它在内存里到底怎么待着的,还是一知半解。今天咱们就把它彻底聊透。
1.1 数组在内存中的连续存储
先问个问题:定义一个数组 int arr[5],内存里到底发生了什么?
答案是:系统会一口气分配5个连续的int大小的空间。注意这个“连续”,这是数组最核心的特征。
我个人习惯把数组想象成一排连在一起的快递柜。每个格子大小一样,编号从0开始。你告诉系统“我要5个int”,它就给你找一排连续的空位,每个空位4字节(32位系统下)。
关键点:数组名arr代表的是整个数组的首地址,也就是第一个元素的地址。arr和&arr[0]的值是一样的,但类型不同——这个细节坑过不少人。
来看个例子:
#include <stdio.h>
int main() {
int arr[5] = {10, 20, 30, 40, 50};
printf("数组首地址: %p\n", arr);
printf("第一个元素地址: %p\n", &arr[0]);
printf("第二个元素地址: %p\n", &arr[1]);
// 看看地址差了多少
printf("地址差: %ld 字节\n",
(long)&arr[1] - (long)&arr[0]);
return 0;
}
输出结果会告诉你:arr和&arr[0]打印出来一模一样,&arr[1]比&arr[0]大4个字节。这就是连续存储的直接证据。
我的经验:我在调试嵌入式通信协议时,经常需要把数据打包成字节流发送。这时候数组的连续存储特性就特别有用——直接用memcpy把整个数组拷进缓冲区,省时省力。但要注意字节序问题,那是另一个话题了。
1.2 数组元素的随机访问原理
为什么数组访问那么快?arr[3]为什么能瞬间拿到值?
说白了,就是一条公式的事:
第i个元素的地址 = 数组首地址 + i × 每个元素的大小
你想想看,这只是一个加法和一个乘法。CPU做这个快得跟闪电似的。这就是所谓的“随机访问”——不管你访问第0个还是第9999个,速度都一样。
用代码验证一下:
#include <stdio.h>
int main() {
int arr[5] = {10, 20, 30, 40, 50};
int i = 3;
// 这两种写法等价
printf("arr[%d] = %d\n", i, arr[i]);
printf("*(arr + %d) = %d\n", i, *(arr + i));
// 甚至你可以这么写(虽然没人这么干)
printf("%d[arr] = %d\n", i, i[arr]);
return 0;
}
看到没?arr[i] 本质上就是 *(arr + i)。编译器会把中括号语法直接翻译成指针运算。所以 i[arr] 也能编译通过——因为 i[arr] 被解释成 *(i + arr),和 *(arr + i) 完全一样。
注意:虽然 i[arr] 语法上合法,但千万别这么写。代码是给人看的,不是用来炫技的。我曾经在一个遗留代码里看到这种写法,排查了半天才反应过来——嗯,从那以后我写代码就更“老实”了。
下面这张图展示了数组在内存中的布局和访问方式:
1.3 数组的边界与越界
这是C语言里最危险的地方之一。C不会帮你检查数组下标是否合法——它信任你,或者说它懒得管你。
你声明了 int arr[5],然后写了 arr[5] = 100。编译器不会报错,程序可能正常运行,也可能当场崩溃,更可怕的是——它可能偶尔崩溃,让你抓狂。
#include <stdio.h>
int main() {
int arr[3] = {1, 2, 3};
int secret = 0xDEAD;
// 越界写入
arr[3] = 0xBEAF; // 危险!
arr[4] = 0xCAFE; // 更危险!
arr[-1] = 0; // 负下标?也能编译!
printf("secret = 0x%X\n", secret);
// 猜猜secret变成了什么?
return 0;
}
这段代码在栈上,arr和secret可能挨着。arr[3]越界写入,恰好改写了secret的值。这就是典型的“缓冲区溢出”——安全漏洞的温床。
血的教训:我曾经在一个嵌入式项目里,因为数组越界写了一个字节,导致系统运行3小时后随机死机。排查了整整两天,最后用内存监视工具才发现是某个循环的边界条件多算了1。从那以后,我写循环都养成了一个习惯:for(i = 0; i < N; i++),永远用小于号,绝不用小于等于。
越界的后果分三种:
| 越界类型 | 可能后果 | 发生概率 |
|---|---|---|
| 读越界 | 读到垃圾值,程序行为异常 | 高 |
| 写越界(轻微) | 覆盖相邻变量,逻辑错误 | 中 |
| 写越界(严重) | 覆盖函数返回地址,程序崩溃或被攻击 | 低但致命 |
避坑指南:
- 用宏或常量定义数组大小,别写魔法数字
- 循环边界用
i < N而不是i <= N-1 - 敏感数据(如密码)用完立即清零,防止被越界读到
- 嵌入式开发中,开启编译器的数组边界检查选项(如果有的话)
最后说一句:数组越界是C语言的“特色”,也是它的“痛点”。用好它,你能写出高效的底层代码;用不好,它就是定时炸弹。我个人建议:在性能允许的前提下,多写几个断言(assert)检查下标范围,这习惯能救你一命。
公众号:蓝海资料掘金营,微信deep3321