3. 数组的边界之外:数组下标越界访问、多维数组的越界、变长数组(VLA)的陷阱
数组越界,这大概是C语言里最经典、也最隐蔽的未定义行为之一了。我入行那会儿,第一次被领导叫去排查一个“随机死机”的bug,折腾了两天,最后发现就是数组下标多了一个1。嗯,从那以后,我对数组边界就有了“心理阴影”。
说白了,C语言不像Java或Python,它不会在运行时帮你检查下标是否合法。你写 arr[10],哪怕数组只有5个元素,编译器也不会报错。它只会默默地访问那个内存地址——至于那个地址里存的是什么,天知道。
3.1 一维数组的下标越界
先看个最简单的例子:
#include <stdio.h>
int main() {
int arr[5] = {10, 20, 30, 40, 50};
// 越界访问
for (int i = 0; i <= 5; i++) {
printf("arr[%d] = %d\n", i, arr[i]);
}
return 0;
}
你看,循环条件写成了 i <= 5,而不是 i < 5。当 i = 5 时,访问的是 arr[5],这已经越界了。在我的项目经验里,这种“差一错误”(off-by-one error)是最常见的越界原因。
- 读到一个垃圾值
- 覆盖了相邻变量的值
- 破坏了栈上的返回地址(导致函数返回时跳转到奇怪的地方)
- 在极端情况下,被黑客利用进行缓冲区溢出攻击
为什么会这样?因为数组在内存中是连续存放的。你访问 arr[5],实际上就是访问 arr 起始地址偏移 5 * sizeof(int) 的位置。那个位置可能是另一个变量,也可能是栈上的其他数据。
3.2 多维数组的越界
多维数组的越界,本质上和一维数组是一样的。因为C语言的多维数组在内存中是按行优先(row-major)顺序存储的。你想想看:
int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
// 越界访问
int val = matrix[3][0]; // 访问了第4行,但只有3行
这里 matrix[3][0] 实际上访问的是 matrix 起始地址偏移 3 * 4 * sizeof(int) 的位置。这个位置已经超出了整个数组的存储范围。
我个人习惯在写多维数组循环时,总是用 < 而不是 <=,并且把数组维度定义成宏或常量,避免硬编码。
3.3 变长数组(VLA)的陷阱
变长数组是C99引入的特性,允许你在运行时指定数组大小。听起来很方便,对吧?但这里面的坑,比固定长度数组只多不少。
void process_data(int n) {
int vla[n]; // VLA,大小由运行时参数决定
for (int i = 0; i < n; i++) {
vla[i] = i * 2;
}
// 越界!如果 n 是负数或0呢?
vla[n] = 100; // 未定义行为
}
你看,VLA的大小是运行时决定的。如果 n 是负数,或者 n 非常大(比如用户输入了一个巨大的值),会发生什么?
- n 为负数或0: 标准规定,VLA的大小必须为正整数。如果为0或负数,行为是未定义的。有些编译器会直接报错,有些则可能分配一个奇怪大小的数组。
- n 过大: VLA是在栈上分配的。如果
n太大,会导致栈溢出(stack overflow),程序直接崩溃。 - 越界访问: 和固定数组一样,VLA也不做边界检查。访问
vla[n]仍然是未定义行为。
3.4 避坑指南
说了这么多问题,那怎么避免呢?我总结了几条实用的建议:
- 使用宏或常量定义数组大小,不要硬编码数字。比如
#define ARRAY_SIZE 100,然后所有循环都用ARRAY_SIZE作为边界。 - 循环条件统一用
<而不是<=。这能避免最常见的“差一错误”。 - 对VLA的输入参数做合法性检查。如果
n来自用户输入或外部数据,一定要验证它是否在合理范围内。 - 考虑使用动态内存分配代替VLA。用
malloc在堆上分配内存,虽然多了一点手动释放的麻烦,但更安全可控。 - 启用编译器的边界检查选项。比如GCC的
-fsanitize=address,可以在运行时检测越界访问。
我曾经... 接手过一个嵌入式项目,代码里大量使用了VLA。结果在某个极端情况下,输入参数被篡改成了一个巨大的值,导致栈溢出,系统直接死机。从那以后,我在嵌入式代码中彻底禁用了VLA,全部改用静态数组或动态分配。
3.5 知识体系图
下面这张图总结了数组越界相关的核心知识点和它们之间的关系:
数组越界这个问题,说大不大,说小不小。它不像空指针解引用那样会立刻崩溃,但造成的后果往往更隐蔽、更难排查。我个人觉得,写C代码时对数组操作保持“敬畏之心”是很有必要的——每次写下 arr[i] 的时候,都问自己一句:这个 i 真的在合法范围内吗?