数组访问与越界
C 语言不进行数组边界检查,访问超出数组范围的元素是未定义行为。这种设计是为了性能——每次数组访问都检查边界会显著降低程序速度。但这也意味着程序员必须自己确保索引在有效范围内,否则可能导致数据损坏、安全漏洞或程序崩溃。
下标访问
数组元素通过下标运算符 [] 访问:
int arr[5] = {10, 20, 30, 40, 50};
printf("%d\n", arr[0]); /* 10 */
printf("%d\n", arr[4]); /* 50 */
有效下标范围是 0 到 N-1(N 是数组大小)。
越界访问
访问超出范围的元素是未定义行为:
int arr[5] = {10, 20, 30, 40, 50};
/* arr[5] 越界! */
printf("%d\n", arr[5]); /* 未定义行为 */
arr[5] = 100; /* 未定义行为:可能覆盖其他数据 */
越界访问可能:
- 读取/写入相邻的内存(破坏其他变量)
- 触发段错误(Segmentation Fault)
- 在调试模式下"恰好工作",发布模式下崩溃
- 被利用为安全漏洞(缓冲区溢出攻击)
负数下标
负数下标也是未定义行为:
int arr[5] = {10, 20, 30, 40, 50};
printf("%d\n", arr[-1]); /* 未定义行为 */
但指针运算中负数偏移是合法的(如果指向数组内部):
int arr[5] = {10, 20, 30, 40, 50};
int *p = &arr[2]; /* p 指向 30 */
printf("%d\n", p[-1]); /* 20:合法,p[-1] 等价于 *(p-1) */
printf("%d\n", p[1]); /* 40:合法 */
越界检测
C 语言标准不提供边界检查,但可以通过代码审查和工具检测:
代码中的检查:
void set_element(int arr[], int n, int index, int value)
{
if (index < 0 || index >= n) {
printf("Error: index %d out of bounds [0, %d)\n", index, n);
return;
}
arr[index] = value;
}
编译器选项:
/* GCC 的栈保护 */
gcc -fstack-protector-strong program.c
/* 地址 sanitizer(运行时检测) */
gcc -fsanitize=address program.c
静态分析工具:
- Clang Static Analyzer
- Coverity
- PVS-Studio
常见越界场景
循环边界错误:
int arr[5];
/* 差一错误:访问 arr[5] */
for (int i = 0; i <= 5; i++) /* 应该是 i < 5 */
arr[i] = i;
/* 正确 */
for (int i = 0; i < 5; i++)
arr[i] = i;
字符串操作:
char str[5] = "Hello"; /* '\0' 被截断,str 不是有效字符串 */
strlen(str); /* 未定义行为:找不到 '\0' */
strcpy(str, "World"); /* 如果 str 有 5 字节,'\0' 写入第 6 字节,越界 */
函数参数丢失大小:
void process(int arr[]) /* arr 退化为指针,不知道大小 */
{
arr[10] = 0; /* 可能越界! */
}
/* 正确:传递大小 */
void process(int arr[], int n)
{
if (n > 10) /* 检查 */
arr[10] = 0;
}
安全函数
C11 引入了边界检查接口(可选),但 C99 没有。在 C99 中,使用安全的字符串函数:
/* 不安全 */
strcpy(dest, src); /* 不检查 dest 大小 */
strcat(dest, src); /* 不检查 dest 大小 */
gets(str); /* 已移除,极其危险 */
/* 安全替代 */
strncpy(dest, src, sizeof(dest) - 1); /* 限制复制长度 */
dest[sizeof(dest) - 1] = '\0'; /* 确保终止 */
strncat(dest, src, sizeof(dest) - strlen(dest) - 1);
fgets(str, sizeof(str), stdin); /* 限制读取长度 */
/* C99 snprintf */
snprintf(dest, sizeof(dest), "%s", src); /* 安全格式化 */
常见错误
忘记数组从 0 开始:
int arr[5];
for (int i = 1; i <= 5; i++) /* 错误:漏了 arr[0],多了 arr[5] */
arr[i] = 0;
sizeof 计算错误:
void func(int arr[])
{
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
/* 错误:sizeof(arr) = sizeof(int*) */
arr[i] = 0;
}
指针运算越界:
int arr[5];
int *p = arr;
for (int i = 0; i <= 5; i++)
*p++ = i; /* 最后一次 *p = arr[5],越界 */
最佳实践
- 始终确保索引在 [0, N-1] 范围内
- 数组作为函数参数时,始终传递大小
- 使用
const修饰不修改的数组参数 - 字符串操作使用带长度限制的函数(
strncpy、strncat、fgets、snprintf) - 开启编译器栈保护和地址 sanitizer
- 使用静态分析工具检查越界
- 循环边界用
<而非<=(i < n比i <= n-1更清晰)