7、数组与指针安全:数组越界访问、指针算术错误、多维数组的安全使用
数组和指针,是C语言的灵魂,也是无数bug的温床。我做了十几年安全审计,见过太多系统崩溃、数据泄露,甚至远程代码执行,根源就是数组越界或者指针乱飞。说白了,这两个东西用好了是利器,用不好就是自残。
今天咱们就好好聊聊,怎么安全地驾驭它们。
7.1 数组越界访问:那个“差一”的错误
数组越界,C语言世界里最常见的“杀手”。为什么?因为C语言不检查边界。你写 arr[10],它不会告诉你“嘿,数组只有10个元素,下标最大是9”。它只会默默地访问那块内存,然后……一切皆有可能。
我个人习惯,每次写循环遍历数组,都会下意识地检查一下边界条件。你想想看,下面这段代码,问题出在哪?
#include <stdio.h>
int main() {
int arr[10];
for (int i = 0; i <= 10; i++) { // 注意这里!
arr[i] = i * 2;
}
return 0;
}
嗯,i <= 10 就是罪魁祸首。当 i 等于10时,访问的是 arr[10],而数组最后一个有效元素是 arr[9]。这就是经典的“差一错误”(Off-by-one error)。
我在项目中遇到过,一个同事写的网络协议解析模块,就是因为这个“差一”,导致解析一个特殊构造的数据包时,程序直接崩溃。后来查了三天才定位到问题。
7.2 指针算术错误:一步踏错,万丈深渊
指针算术,说白了就是通过移动指针来访问不同的内存单元。但这里面的坑,比数组还多。
先看一个基础但容易犯错的地方:
#include <stdio.h>
int main() {
int arr[] = {10, 20, 30, 40, 50};
int *p = arr;
// 正确:访问第三个元素
printf("%d\n", *(p + 2)); // 输出 30
// 错误:试图访问越界位置
printf("%d\n", *(p + 10)); // 未定义行为!
return 0;
}
为什么会这样?因为 p + 10 这个地址,根本不在数组的管辖范围内。指针算术的偏移量,是以“指针指向的类型大小”为单位的。对于 int*,p + 1 实际上移动了 sizeof(int) 个字节(通常是4字节)。
我曾经犯过一个低级错误:
// 错误示例:试图用指针遍历结构体数组
struct Student {
int id;
char name[32];
float score;
};
struct Student class[50];
struct Student *ptr = class;
// 我想访问第5个学生的id
int *id_ptr = (int*)((char*)ptr + 5 * sizeof(struct Student)); // 危险!
这种“手动计算偏移”的做法,极其危险。一旦结构体有内存对齐,或者我算错了偏移量,结果就是访问到错误的数据,甚至越界。
ptr[5],编译器会帮你算好。既安全,又清晰。
7.3 多维数组的安全使用:别被“维”绕晕
多维数组,本质上还是一维数组,只是编译器帮我们做了“行优先”的映射。理解这一点,很多问题就迎刃而解了。
比如一个 int matrix[3][4],在内存中是连续存放的:
matrix[0][0], matrix[0][1], ..., matrix[0][3],
matrix[1][0], matrix[1][1], ..., matrix[1][3],
matrix[2][0], matrix[2][1], ..., matrix[2][3]
访问 matrix[i][j] 时,编译器实际计算的是 *( *(matrix + i) + j )。这里最容易出错的,就是指针类型不匹配。
看这个例子:
#include <stdio.h>
void print_matrix_wrong(int **mat, int rows, int cols) {
// 错误!二维数组名不是 int**
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf("%d ", mat[i][j]); // 编译警告或崩溃
}
}
}
void print_matrix_correct(int rows, int cols, int mat[rows][cols]) {
// 正确:使用变长数组参数
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf("%d ", mat[i][j]);
}
printf("\n");
}
}
int main() {
int matrix[3][4] = {{1,2,3,4}, {5,6,7,8}, {9,10,11,12}};
// print_matrix_wrong(matrix, 3, 4); // 不要这样调用!
print_matrix_correct(3, 4, matrix);
return 0;
}
我个人建议,传递多维数组时,要么用变长数组(VLA)参数,要么用一维数组加手动索引计算。千万别用 int**,那是给指针数组用的,不是给二维数组用的。
下面这张图,帮你理清多维数组在内存中的布局和访问方式:
7.4 避坑指南与最佳实践
说了这么多问题,咱们总结一下,到底该怎么安全地使用数组和指针。
核心原则: 永远假设用户输入是恶意的,永远检查边界。
| 场景 | 安全做法 | 常见错误 |
|---|---|---|
| 数组遍历 | 使用 for (i = 0; i < size; i++) |
使用 i <= size 或 i < size-1 |
| 指针偏移 | 用下标 p[i] 代替 *(p + i) |
手动计算字节偏移 |
| 多维数组传参 | 使用变长数组参数或一维数组+手动索引 | 使用 int** |
| 动态分配数组 | 分配后立即检查 NULL,并记录大小 |
忘记检查 malloc 返回值 |
sizeof 操作符。
最后,送你一个“安全三问”:
- 这个指针指向的内存,真的属于我吗?(所有权问题)
- 我要访问的位置,在合法范围内吗?(边界问题)
- 如果用户故意输入一个很大的值,我的代码会崩溃吗?(鲁棒性问题)
每次写数组或指针操作前,问自己一遍。养成习惯,能帮你避开90%的坑。
公众号:蓝海资料掘金营,微信deep3321