数组越界访问:一维数组越界、二维数组越界、字符串数组越界

数组越界,这大概是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++)或动态分配

核心知识体系

下面这张图总结了数组越界的几种典型场景和它们的后果:

数组越界核心知识体系 一维数组越界 二维数组越界 字符串数组越界 下标从0开始,arr[n]越界 循环条件写错(i <= n) 行/列下标混淆 线性存储,对齐问题 忘记'\0'结尾 缓冲区溢出 后果:程序崩溃 | 数据损坏 | 安全漏洞 | 难以调试 预防:边界检查 | 安全函数 | 静态分析工具 | 代码审查

如何排查数组越界

说实话,数组越界bug最难查。因为它不会每次都复现,而且报错的位置往往离真正出问题的地方很远。我总结了几条经验:

  1. 使用AddressSanitizer:编译时加 -fsanitize=address,它能精确检测出越界访问的位置。我在项目中几乎必开这个选项。
  2. 加断言:在访问数组前,手动检查下标范围。虽然会影响性能,但调试阶段值得。
  3. 看崩溃时的堆栈:如果程序在 free() 或 malloc() 时崩溃,十有八九是之前有越界写操作,把堆管理结构给破坏了。
  4. 用Valgrind:虽然慢,但能检测出很多内存问题。我一般会在CI流程中跑一遍Valgrind。
📌 核心要点
- 数组越界是未定义行为,编译器不会帮你检查
- 越界读可能拿到垃圾数据,越界写可能破坏其他变量
- 字符串数组的'\0'结尾是高频踩坑点
- 调试工具(ASan、Valgrind)是你的好朋友

嗯,数组越界这个话题,说深了可以写一本书。但核心就一句话:永远不要假设你的下标是安全的。每次写数组访问代码时,多问自己一句:「这个下标会不会越界?」养成这个习惯,能帮你省下无数调试时间。