3. 指针与数组:数组名的本质
数组和指针,在C语言里就像一对双胞胎。长得像,但性格完全不同。很多初学者在这里栽跟头,说实话,我当年也迷糊过好一阵子。
咱们今天就把这事儿彻底说清楚。
3.1 数组名的本质:首元素地址
先问一个问题:int arr[5] 里的 arr 到底是什么?
很多人脱口而出:「数组名就是首地址。」
嗯,对了一半。更准确地说——数组名是首元素的地址。注意,是「首元素」,不是「数组」本身。
核心结论:
arr等价于&arr[0]arr + 1跳过一个元素(而不是跳过整个数组)&arr才是整个数组的地址(类型是int (*)[5])
我举个例子你就明白了:
#include <stdio.h>
int main() {
int arr[5] = {10, 20, 30, 40, 50};
printf("arr = %p\n", arr); // 首元素地址
printf("&arr[0] = %p\n", &arr[0]); // 和上面一样
printf("&arr = %p\n", &arr); // 整个数组的地址
printf("arr + 1 = %p\n", arr + 1); // 加 4 字节(int)
printf("&arr + 1 = %p\n", &arr + 1); // 加 20 字节(整个数组)
return 0;
}
你看,arr + 1 和 &arr + 1 的结果完全不同。前者跳过一个 int,后者跳过整个数组。这就是本质区别。
我曾经踩过的坑:
有一次我写一个数组拷贝函数,传参时用了 void func(int arr[])。结果在函数内部用 sizeof(arr) 想算数组长度,死活不对。后来才意识到——数组作为函数参数时,已经退化成指针了。sizeof(arr) 返回的是指针大小,不是数组大小。
3.2 指针访问数组元素
既然数组名就是首元素地址,那用指针访问数组元素就顺理成章了。
两种写法完全等价:
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
// 写法一:下标法
printf("%d\n", arr[2]); // 3
printf("%d\n", p[2]); // 3
// 写法二:指针偏移法
printf("%d\n", *(arr + 2)); // 3
printf("%d\n", *(p + 2)); // 3
我个人习惯用下标法,可读性更好。但在底层驱动开发中,指针偏移法更常见,因为有时候你需要手动控制地址偏移量。
比如我在做嵌入式项目时,经常要遍历一个缓冲区:
uint8_t buffer[256];
uint8_t *ptr = buffer;
for (int i = 0; i < 256; i++) {
*(ptr + i) = read_sensor(); // 指针偏移
}
这种写法在汇编层面效率更高,因为编译器可以直接生成基址+偏移的寻址指令。
3.3 指针与多维数组
多维数组,说白了就是「数组的数组」。比如 int matrix[3][4],它包含 3 个元素,每个元素是一个长度为 4 的 int 数组。
那 matrix 是什么?
matrix 是首元素的地址。首元素是什么?是 matrix[0],它是一个长度为 4 的 int 数组。所以 matrix 的类型是 int (*)[4]——指向长度为 4 的 int 数组的指针。
我画了一张图,帮你理清这个关系:
你看,matrix 指向第一行,matrix + 1 指向第二行。每一行是一个一维数组,所以 *(matrix + 1) + 2 就是第二行第三个元素的地址,再解引用就是值。
说白了,多维数组的指针操作就是「层层剥开」的过程。
3.4 数组指针 vs 指针数组
这两个概念,名字就差一个字,但完全是两码事。我见过太多人搞混了。
| 概念 | 定义 | 本质 | 示例 |
|---|---|---|---|
| 数组指针 | 指向数组的指针 | 一个指针,指向一个数组 | int (*p)[5] |
| 指针数组 | 存放指针的数组 | 一个数组,元素是指针 | int *p[5] |
怎么记?我有个小技巧:看变量名和谁结合。
int (*p)[5]:*p先结合,说明 p 是指针,指向一个长度为 5 的 int 数组。这是数组指针。int *p[5]:p[5]先结合,说明 p 是数组,元素类型是int *。这是指针数组。
运算符优先级决定了这一切。[] 优先级高于 *,所以 int *p[5] 先被解释为数组,而 int (*p)[5] 用括号强行让 * 先结合。
实用建议:
数组指针常用于二维数组传参。比如:
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");
}
}
这样传参,函数内部就知道每行有 4 个元素,可以正确计算偏移。
指针数组呢?我常用在字符串处理中。比如:
const char *weekdays[] = {
"Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday", "Sunday"
};
for (int i = 0; i < 7; i++) {
printf("%s\n", weekdays[i]);
}
每个元素是一个 const char *,指向一个字符串常量。这样比二维字符数组灵活得多,字符串长度可以不同。
我曾经犯过的错:
有一次我写 int *p[5] 想定义一个指向数组的指针,结果编译通过但运行结果完全不对。后来才发现我写的是指针数组,里面 5 个指针都没初始化,乱指一气。从那以后,我每次写这类声明都会先默念一遍「运算符优先级」。
小结
这一节的内容,说白了就是三件事:
- 数组名是首元素地址,不是数组本身的地址。
&arr才是整个数组的地址。 - 指针访问数组有两种方式:下标法和偏移法,本质一样。
- 数组指针 vs 指针数组:看括号和优先级,一个是指向数组的指针,一个是存指针的数组。
这些概念在嵌入式开发中天天用。比如操作寄存器缓冲区、处理传感器数据流、解析通信协议帧……说白了,指针和数组的关系搞不清楚,写出来的代码要么跑飞,要么效率低得吓人。
嗯,这一节就到这儿。下一节咱们聊聊指针和函数的关系,那又是一个容易踩坑的地方。
公众号:蓝海资料掘金营,微信deep3321