3. 数组与指针的关系:数组名退化为指针、下标访问的本质、多维数组的指针操作
数组和指针,在C/C++里就像一对双胞胎。长得像,但性格完全不同。很多新手在这里栽跟头,说实话,我当年也踩过不少坑。今天咱们就把这层窗户纸捅破。
3.1 数组名到底是什么?
先问一个问题:int arr[5] 里的 arr 是什么?
很多人脱口而出:「数组名就是首地址」。嗯,对了一半。更准确地说:数组名在大多数表达式中会「退化」为指向首元素的指针。
但注意——有两个例外:
sizeof(arr)返回整个数组的大小,不是指针大小&arr取的是整个数组的地址,类型是int (*)[5]
核心结论:数组名不是指针,但用起来像指针。它是个「常量」,不能自增自减。
我在项目中遇到过有人写 arr++ 想遍历数组,结果编译报错。嗯,数组名不是左值,不能修改。
3.2 下标访问的本质
你写 arr[i] 的时候,编译器到底干了什么?
说白了,arr[i] 就是 *(arr + i) 的语法糖。这是C语言最优雅也最危险的设计之一。
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;
// 下面三种写法完全等价
printf("%d\n", arr[2]); // 30
printf("%d\n", *(arr+2)); // 30
printf("%d\n", p[2]); // 30
你想想看,既然 arr[i] 等价于 *(arr + i),那 i[arr] 呢?
printf("%d\n", 2[arr]); // 30,合法但别这么写
为什么合法?因为 2[arr] 被解释为 *(2 + arr),加法交换律嘛。但我在代码审查里看到这种写法,一定会打回去重写——可读性太差了。
避坑指南:我曾经在维护一个遗留系统时,发现有人用 p[-1] 访问前一个元素。虽然语法上合法,但一旦 p 指向数组开头,这就是越界访问。下标可以是负数,但你必须确保指针指向的位置正确。
3.3 多维数组的指针操作
二维数组的指针操作,是面试高频题,也是实际项目中容易出bug的地方。
先看定义:
int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
matrix 的类型是什么?不是 int**,而是 int (*)[4]——指向「包含4个int的数组」的指针。
我画了一张图帮你理解内存布局:
看到没?matrix 指向第0行的首地址,matrix+1 跳过了整整一行(16字节,假设int占4字节)。
访问元素的几种方式:
// 方式1:下标法(推荐)
int val = matrix[1][2]; // 7
// 方式2:指针法
int val = *(*(matrix + 1) + 2); // 7
// 方式3:用指针遍历
int (*row)[4] = matrix; // 行指针
int *cell = *row; // 解引用得到第0行的首元素指针
cell[6] = 42; // 等价于 matrix[1][2] = 42
我的习惯:在项目里,我尽量用下标法访问多维数组。指针法虽然灵活,但可读性差,而且容易写错括号。你想想看,*(*(matrix + i) + j) 少写一个括号,结果就全错了。
3.4 数组指针 vs 指针数组
这两个概念经常被混淆,我列个表对比一下:
| 概念 | 定义 | 类型 | 示例 |
|---|---|---|---|
| 数组指针 | 指向数组的指针 | int (*p)[5] |
用于二维数组的行遍历 |
| 指针数组 | 存放指针的数组 | int *p[5] |
用于字符串数组等场景 |
怎么记?看 * 和 [] 的优先级:[] 优先级高于 *。所以:
int *p[5]→ p先和[5]结合,是个数组,元素是int*→ 指针数组int (*p)[5]→ 括号让*先和 p 结合,p是指针,指向int[5]→ 数组指针
曾经踩过的坑:我在一个图像处理模块里,需要传递二维数组给函数。我写了 void process(int **data, int rows, int cols),结果传参时编译报错。为什么?因为 int matrix[3][4] 退化后是 int (*)[4],不是 int**。正确的写法是 void process(int (*data)[4], int rows),或者用一维指针模拟:void process(int *data, int rows, int cols),然后通过 data[i*cols + j] 访问。
3.5 实际项目中的注意事项
说几个我实际遇到过的问题:
- 数组作为函数参数:传数组时,它退化为指针。所以
sizeof(arr)在函数内部得到的是指针大小,不是数组大小。我建议额外传一个长度参数。 - 多维数组的动态分配:用
malloc分配二维数组时,很多人写成int **p = malloc(rows * cols * sizeof(int)),然后直接用p[i][j]访问——这是未定义行为!正确做法是分配一个一维数组,手动计算偏移。 - 越界访问的隐蔽性:C/C++ 不检查数组边界。你写
arr[10]可能不会立即崩溃,但会悄悄破坏相邻内存。这种 bug 最难排查。
// 正确的动态二维数组分配方式
int *data = (int*)malloc(rows * cols * sizeof(int));
if (!data) {
// 处理内存分配失败
return;
}
// 访问 data[i][j]
data[i * cols + j] = 42;
// 释放
free(data);
总结一下:数组名退化为指针,下标访问本质是指针运算,多维数组在内存中是线性存储的。理解这些,你就能写出更安全、更高效的代码。嗯,这些概念虽然基础,但我在代码审查中看到的相关 bug 真不少——越界、类型不匹配、内存泄漏,很多都源于对数组和指针关系的模糊理解。
可可精品资料库