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的数组」的指针。

我画了一张图帮你理解内存布局:

二维数组 int matrix[3][4] 内存布局 matrix[0][0] matrix[0][1] matrix[0][2] matrix[0][3] matrix[1][0] matrix[1][1] matrix[1][2] matrix[1][3] matrix[2][0] matrix[2][1] matrix[2][2] matrix[2][3] matrix matrix+1 matrix+2 第0行 第1行 第2行

看到没?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 实际项目中的注意事项

说几个我实际遇到过的问题:

  1. 数组作为函数参数:传数组时,它退化为指针。所以 sizeof(arr) 在函数内部得到的是指针大小,不是数组大小。我建议额外传一个长度参数。
  2. 多维数组的动态分配:用 malloc 分配二维数组时,很多人写成 int **p = malloc(rows * cols * sizeof(int)),然后直接用 p[i][j] 访问——这是未定义行为!正确做法是分配一个一维数组,手动计算偏移。
  3. 越界访问的隐蔽性: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 真不少——越界、类型不匹配、内存泄漏,很多都源于对数组和指针关系的模糊理解。

可可精品资料库