14、数组与指针的等价性:下标运算的本质、指针与数组的互换使用、多维数组的指针等价形式
好,咱们今天聊一个C语言里特别有意思的话题——数组和指针到底是不是一回事?
很多初学者会问:「数组名不就是指针吗?」嗯,这个问题我当年也纠结过。后来在嵌入式项目里踩过坑,才真正搞明白。今天我把这些经验掰开揉碎了讲给你听。
14.1 下标运算的本质:编译器在背后做了什么?
先看一个最基础的问题:a[i] 到底是什么意思?
你写 a[3],编译器其实把它翻译成了 *(a + 3)。没错,下标运算符 [] 的本质就是指针运算的语法糖。
核心公式:
a[i] == *(a + i)
甚至更夸张:i[a] == *(i + a) 也是合法的!
我第一次看到 3[a] 这种写法时,觉得这肯定是语法错误。结果编译通过,运行正常。为什么?因为 [] 就是个加法再解引用,加法是可交换的。
#include <stdio.h>
int main() {
int arr[5] = {10, 20, 30, 40, 50};
// 这三种写法完全等价
printf("%d\n", arr[2]); // 30
printf("%d\n", *(arr + 2)); // 30
printf("%d\n", 2[arr]); // 30 —— 别这么写,但要知道它合法
return 0;
}
我的建议:虽然 i[a] 合法,但千万别在生产代码里这么写。代码是写给人看的,不是写给编译器看的。可读性永远第一。
14.2 指针与数组的互换使用:什么时候可以?什么时候不行?
很多人以为「数组名就是指针」,其实只说对了一半。
数组名在大多数表达式中会「退化」成指向首元素的指针。但有三个例外:
- sizeof 运算符:
sizeof(arr)返回整个数组的大小,不是指针大小 - & 取地址运算符:
&arr的类型是int (*)[5],指向整个数组 - 字符串字面量初始化字符数组:
char s[] = "hello";这里的数组是真正的数组
#include <stdio.h>
int main() {
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr; // arr 退化为指针
printf("sizeof(arr) = %zu\n", sizeof(arr)); // 20 (5 * 4)
printf("sizeof(p) = %zu\n", sizeof(p)); // 8 (64位系统)
// 但下标操作完全一样
printf("arr[2] = %d, p[2] = %d\n", arr[2], p[2]); // 都是 3
return 0;
}
我曾经踩过的坑:在函数参数里写 void func(int arr[]),然后以为 sizeof(arr) 能拿到数组长度。结果呢?arr[] 作为函数参数时,编译器自动把它当作指针处理。sizeof(arr) 返回的是指针大小,不是数组大小。
所以,传递数组时一定要同时传长度参数,或者用 sizeof(arr)/sizeof(arr[0]) 在原作用域里计算。
14.3 多维数组的指针等价形式
二维数组的指针等价性,是很多人绕不过去的坎。咱们一步步拆开看。
假设有:
int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
matrix 的类型是 int [3][4],退化后是 int (*)[4]——指向「包含4个int的数组」的指针。
所以:
matrix等价于&matrix[0],类型int (*)[4]matrix[i]等价于*(matrix + i),类型int [4],退化后是int *matrix[i][j]等价于*(*(matrix + i) + j)
#include <stdio.h>
int main() {
int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
// 访问第2行第3列的元素
printf("%d\n", matrix[1][2]); // 7
printf("%d\n", *(*(matrix + 1) + 2)); // 7
printf("%d\n", *(matrix[1] + 2)); // 7
// 用指针遍历二维数组
int *p = &matrix[0][0];
for(int i = 0; i < 12; i++) {
printf("%d ", p[i]);
}
// 输出:1 2 3 4 5 6 7 8 9 10 11 12
return 0;
}
关键理解:
二维数组在内存中是连续线性存储的。行优先排列。所以你可以用一级指针遍历整个二维数组——前提是你知道总元素个数。
14.4 多维数组作为函数参数
写函数处理二维数组时,很多人会卡在参数声明上。
// 正确写法:必须指定第二维大小
void print_matrix(int mat[][4], int rows) {
for(int i = 0; i < rows; i++) {
for(int j = 0; j < 4; j++) {
printf("%d ", mat[i][j]);
}
printf("\n");
}
}
// 等价写法:用数组指针
void print_matrix2(int (*mat)[4], int rows) {
// 用法完全一样
}
为什么必须指定第二维?因为编译器需要知道每行有多少个元素,才能计算 mat[i][j] 的偏移量。
我个人的习惯:处理多维数组时,如果维度固定,我会用 typedef 定义类型别名,代码会清晰很多。
typedef int Row[4]; // Row 是包含4个int的数组类型
void print_matrix(Row mat[], int rows);
14.5 知识体系总览
下面这张图帮你理清数组与指针等价性的核心脉络:
14.6 实战中的避坑指南
最后,分享几个我实际项目中遇到的教训:
- 别对数组名用 ++:数组名不是左值,不能自增。但指针可以。
- 二维数组传参时,第二维必须常量:C99 支持变长数组(VLA),但可移植性差,慎用。
- 用指针遍历二维数组时,注意边界:我曾经写了个
p[i][j]的嵌套循环,结果 p 是一级指针,编译直接报错——类型不匹配。
我曾经犯过的错:在一个通信协议解析模块里,我把接收缓冲区定义成 uint8_t buf[256],然后在另一个文件里用 extern uint8_t *buf 声明。结果 sizeof 行为完全不一样,导致缓冲区溢出。血的教训——extern 声明要和定义保持一致。
嗯,数组和指针的等价性,说白了就是理解编译器怎么算地址。你把这个搞透了,后面看任何复杂的指针声明都不会怵。
公众号:蓝海资料掘金营,微信deep3321