12、数组越界:为什么 C 语言不检查数组越界?

这个问题,我几乎每次给新人培训时都会被问到。

“为什么我写了个 arr[10],数组明明只有 5 个元素,程序居然还能跑?”

嗯,更可怕的是——有时候能跑,有时候崩,有时候半夜才崩。

先看一个经典例子

#include <stdio.h>

int main() {
    int arr[3] = {1, 2, 3};
    for (int i = 0; i <= 3; i++) {
        arr[i] = i * 10;
        printf("arr[%d] = %d\n", i, arr[i]);
    }
    return 0;
}

这段代码,i = 3 时已经越界了。但很多编译器不会报错,程序照常运行。

为什么?

C 语言的设计哲学:信任程序员

C 语言诞生于上世纪 70 年代。那时候内存是金贵的,CPU 是慢的。

如果每次数组访问都加一个边界检查,会多出几条指令。你想想看,一个循环跑 10 万次,多出来的开销就非常可观了。

说白了,C 语言的设计者选择了“效率优先”。他们相信你——程序员——知道自己数组有多大。

核心观点:C 语言不检查数组越界,不是“忘了”,而是“故意不检查”。

这是一种权衡:用潜在的安全风险,换取极致的运行效率。

越界之后发生了什么?

数组在内存中是连续存放的。你写 arr[5],编译器就老老实实计算:arr 的起始地址 + 5 * sizeof(int)

它不管这个地址是不是属于你的数组。它只管算地址、读写数据。

所以越界之后,你实际上在读写“别人的内存”。可能是:

  • 另一个局部变量
  • 函数的返回地址
  • 堆上的其他数据
  • 甚至其他进程的内存(如果 MMU 没拦住)

我在项目中遇到过最诡异的一次:一个数组越界,把隔壁变量的值改了。那个变量是个循环计数器。结果循环次数变了,程序行为完全失控。查了整整两天才定位到问题。

为什么有些语言会检查,C 不检查?

语言 数组越界检查 代价
C / C++ 不检查 零运行时开销
Java 检查,抛异常 每次访问多几条指令
Python 检查,抛异常 解释执行,开销更大
Rust 检查(debug 模式) release 模式可移除

你看,C 语言站在了最左边。它把安全的责任完全交给了你。

越界的后果:从“没事”到“灾难”

越界不一定会立刻崩溃。这恰恰是最坑的地方。

  • 写越界 1 个字节:可能改了某个标志位,程序行为异常
  • 写越界 4 个字节:可能覆盖了函数返回地址,导致跳转到奇怪的地方
  • 写越界 100 个字节:大概率触发段错误(Segmentation Fault)

警告:数组越界是 C 语言中缓冲区溢出漏洞的主要来源。历史上很多著名的安全漏洞(如 Morris 蠕虫、Heartbleed)都跟数组越界有关。

这不是“小问题”,这是“大杀器”。

如何避免数组越界?

我曾经因为一个 off-by-one 错误,让一块嵌入式开发板在高温测试中反复重启。从那以后,我养成了几个习惯:

  1. 使用宏或常量定义数组大小
    #define ARRAY_SIZE 10
    int arr[ARRAY_SIZE];
    for (int i = 0; i < ARRAY_SIZE; i++) {
        // ...
    }
  2. 遍历时用 < 而不是 <=

    这个错误太常见了。记住:数组下标从 0 开始,最大下标是 size - 1

  3. 使用静态分析工具

    比如 cppcheckclang-tidy,它们能发现很多明显的越界问题。

  4. 在关键代码中手动加断言
    #include <assert.h>
    void set_element(int *arr, int index, int value) {
        assert(index >= 0 && index < MAX_SIZE);
        arr[index] = value;
    }

小技巧:在嵌入式开发中,我习惯在调试版本里开启地址消毒器(AddressSanitizer)。它能在运行时检测越界访问,虽然会拖慢速度,但调试阶段非常值得。

一张图看懂数组越界的本质

数组越界:内存访问的本质 合法数组空间 arr[0] ~ arr[4] 越界区域 arr[5] ~ ... 越界写操作 arr[5] = 100 实际内存布局 arr[0] arr[1] arr[2] arr[3] arr[4] arr[5] arr[6] arr[7] 其他变量 C 语言只计算地址,不验证边界 arr[5] 的地址 = arr 基地址 + 5 × sizeof(int) 这个地址可能属于其他变量、栈帧、甚至代码段 越界 = 在别人的地盘上乱写乱画

总结一下

C 语言不检查数组越界,是刻意为之。它把效率放在第一位,把安全交给你。

这就像一把锋利的刀——用好了切菜飞快,用不好就切到手。

我个人习惯是:写循环时多看一眼边界条件,用工具辅助检查,关键地方加断言。这些习惯帮我省下了无数调试时间。

记住:越界不是“会不会发生”的问题,而是“什么时候发生”的问题。早点发现,早点修复,别等到上线了才后悔。


公众号:蓝海资料掘金营,微信deep3321