数组越界访问:一维数组越界、二维数组越界、字符串数组越界
数组越界,这大概是C/C++程序员最熟悉的陌生人。说熟悉,是因为几乎每个人都踩过这个坑;说陌生,是因为很多人踩了坑还不知道自己是怎么掉进去的。我个人习惯把数组越界称为「内存界的幽灵」,它不会立刻报错,但会在你最意想不到的时候,让程序崩溃得一塌糊涂。
一维数组越界:最基础的陷阱
先看一个最简单的例子。我刚开始写C语言时,就犯过这种低级错误:
int arr[5] = {1, 2, 3, 4, 5};
arr[5] = 100; // 越界了!
printf("%d\n", arr[5]);
你想想看,arr[5] 这个位置到底在哪?数组下标从0开始,arr[0] 到 arr[4] 才是合法范围。arr[5] 实际上访问的是数组末尾之后的那块内存。这块内存里原本存着什么?可能是另一个变量,可能是函数返回地址,也可能是空闲内存。
我在项目中遇到过这样一个案例:一个同事写了个排序函数,循环条件写成了 i <= n 而不是 i < n。结果每次排序都会把数组后面的一个「无辜」变量给改掉。那个变量恰好是循环计数器——好家伙,程序直接死循环了。
我曾经在调试一个网络协议栈时,发现数据包总是莫名其妙地多出几个字节。查了三天,最后发现是一个数组下标写成了
buffer[length] 而不是 buffer[length-1]。那个多出来的字节,恰好覆盖了下一个数据包的头部校验和。嗯,这种bug最难查,因为它不会每次都崩溃。
二维数组越界:二维的思维,一维的内存
二维数组在内存中其实是线性存储的。说白了,就是按行优先的方式,把二维铺成一维。很多人不理解这一点,所以容易犯两种错误。
第一种:行和列搞反
int matrix[3][4]; // 3行4列
// 错误写法
matrix[4][2] = 10; // 行越界了!
// 正确写法
matrix[2][4] = 10; // 列越界了!
等等,上面两个都是错的。第一个是行下标越界(最大行下标是2),第二个是列下标越界(最大列下标是3)。
第二种:把二维数组当一维数组用
int matrix[3][4];
// 危险操作:直接遍历所有元素
for (int i = 0; i < 12; i++) {
*((int*)matrix + i) = i; // 虽然能工作,但很危险
}
为什么说危险?因为编译器可能会在数组之间插入对齐填充字节。我见过一个项目,在ARM平台上这样写没问题,换到x86平台就崩了——因为两个平台的对齐规则不一样。
处理二维数组时,永远使用两层循环,不要耍小聪明用指针偏移。代码可读性比那点性能提升重要得多。
字符串数组越界:C语言的经典陷阱
字符串在C语言里就是字符数组,以'\0'结尾。这个'\0'经常被人遗忘。
char str[10];
strcpy(str, "Hello World!"); // 字符串长度12,加上'\0'是13,越界了!
为什么会这样?"Hello World!" 有12个字符,加上结尾的'\0'一共13个字节。而 str 只有10个字节的空间。这会导致缓冲区溢出——一个非常严重的安全漏洞。
我记得有一次做嵌入式开发,一个同事用 sprintf 拼接字符串,没注意缓冲区大小。结果程序运行一段时间后,系统日志突然开始乱码。查到最后发现,溢出的数据覆盖了任务控制块(TCB),导致任务调度器发疯。
我个人的习惯是:
- 使用
strncpy 代替 strcpy- 使用
snprintf 代替 sprintf- 永远给字符串数组多留一个字节给'\0'
- 如果可能,用
std::string(C++)或动态分配
核心知识体系
下面这张图总结了数组越界的几种典型场景和它们的后果:
如何排查数组越界
说实话,数组越界bug最难查。因为它不会每次都复现,而且报错的位置往往离真正出问题的地方很远。我总结了几条经验:
- 使用AddressSanitizer:编译时加
-fsanitize=address,它能精确检测出越界访问的位置。我在项目中几乎必开这个选项。 - 加断言:在访问数组前,手动检查下标范围。虽然会影响性能,但调试阶段值得。
- 看崩溃时的堆栈:如果程序在
free()或malloc()时崩溃,十有八九是之前有越界写操作,把堆管理结构给破坏了。 - 用Valgrind:虽然慢,但能检测出很多内存问题。我一般会在CI流程中跑一遍Valgrind。
- 数组越界是未定义行为,编译器不会帮你检查
- 越界读可能拿到垃圾数据,越界写可能破坏其他变量
- 字符串数组的'\0'结尾是高频踩坑点
- 调试工具(ASan、Valgrind)是你的好朋友
嗯,数组越界这个话题,说深了可以写一本书。但核心就一句话:永远不要假设你的下标是安全的。每次写数组访问代码时,多问自己一句:「这个下标会不会越界?」养成这个习惯,能帮你省下无数调试时间。