3、指针与一维数组:数组名就是首地址、指针遍历数组、指针运算(++、--、+、-)、数组下标与指针的关系
好,咱们今天聊一个C语言里最基础、也最容易被绕晕的话题——指针和一维数组的关系。
我记得刚入行那会儿,有个老工程师跟我说过一句话:「数组和指针,你搞懂了,C语言就算入门了。」我当时还不信,觉得不就是个地址嘛。结果后来在嵌入式项目里,因为数组越界导致整个系统跑飞,排查了整整两天……嗯,从那以后我再也不敢小看这两个东西了。
3.1 数组名就是首地址
先问一个问题:int arr[5] = {10, 20, 30, 40, 50}; 这个 arr 到底是什么?
很多人会告诉你:「数组名就是数组的首地址。」这话对,但不全对。准确地说,数组名代表的是整个数组对象的首地址,也就是第一个元素的地址。
核心结论:
arr等价于&arr[0]arr的类型是int*(指向 int 的指针)&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);
// 三个值打印出来是一样的
// 但含义不同!
return 0;
}
你看,三个地址打印出来数值相同。但 arr+1 和 &arr+1 跳过的字节数完全不同。前者跳过一个 int(4字节),后者跳过整个数组(20字节)。我在项目中就见过有人把这两个搞混,结果指针偏移算错了,数据全读歪了。
3.2 指针遍历数组
既然数组名就是首地址,那我们完全可以用一个指针来遍历数组。说白了,指针就是「会走路的地址」。
#include <stdio.h>
int main() {
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr; // p 指向数组首元素
for (int i = 0; i < 5; i++) {
printf("arr[%d] = %d, *(p+%d) = %d\n", i, arr[i], i, *(p + i));
}
return 0;
}
输出结果:
arr[0] = 10, *(p+0) = 10
arr[1] = 20, *(p+1) = 20
arr[2] = 30, *(p+2) = 30
arr[3] = 40, *(p+3) = 40
arr[4] = 50, *(p+4) = 50
看到了吗?arr[i] 和 *(p+i) 完全等价。编译器在处理 arr[i] 时,其实就是在做 *(arr + i) 这件事。你想想看,数组下标本质上就是指针运算的语法糖。
个人习惯:在嵌入式开发中,我更喜欢用指针遍历数组,尤其是处理连续内存块(比如 DMA 传输、缓冲区操作)时。指针自增比下标索引更直观,而且生成的汇编代码往往更紧凑。
3.3 指针运算:++、--、+、-
指针的加减运算,不是简单的整数加减。它是以「指向的数据类型大小」为步长的。这一点特别重要,我见过太多新手在这里翻车。
| 运算 | 含义 | 示例(int *p) |
|---|---|---|
p + n |
指针向后移动 n 个元素 | p + 1 地址增加 4 字节 |
p - n |
指针向前移动 n 个元素 | p - 1 地址减少 4 字节 |
p++ |
先取值,再自增 | int val = *p++; |
++p |
先自增,再取值 | int val = *++p; |
p-- |
先取值,再自减 | int val = *p--; |
--p |
先自减,再取值 | int val = *--p; |
#include <stdio.h>
int main() {
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;
// 使用 p++ 遍历
printf("正向遍历:\n");
for (int i = 0; i < 5; i++) {
printf("%d ", *p++); // 先取当前值,p 再自增
}
printf("\n");
// 此时 p 已经越界,需要重新指向
p = &arr[4];
// 使用 p-- 反向遍历
printf("反向遍历:\n");
for (int i = 0; i < 5; i++) {
printf("%d ", *p--); // 先取当前值,p 再自减
}
printf("\n");
return 0;
}
我曾经踩过的坑:有一次在 STM32 上做环形缓冲区,用 *p++ 写数据,结果忘记检查指针是否越界。程序跑了几个小时,指针飘到了栈空间,把函数返回地址给覆盖了。嗯,从那以后我每次用指针遍历,都会加边界检查。
3.4 数组下标与指针的关系
咱们把话说透:arr[i] 本质上就是 *(arr + i)。编译器看到 arr[i],直接翻译成 *(arr + i)。所以你可以写出下面这种「看起来很怪但完全合法」的代码:
#include <stdio.h>
int main() {
int arr[5] = {10, 20, 30, 40, 50};
// 下面两种写法完全等价
printf("arr[2] = %d\n", arr[2]); // 30
printf("2[arr] = %d\n", 2[arr]); // 30,你没看错!
// 因为 arr[2] 等价于 *(arr + 2)
// 而 2[arr] 等价于 *(2 + arr)
// 加法交换律,结果一样
return 0;
}
当然,我建议你不要在项目里写 2[arr] 这种代码。虽然语法正确,但可读性太差了。团队里其他人看到,估计会想打人。
3.5 知识体系图
下面这张图把本章的核心逻辑串起来了,你可以对照着看:
3.6 实战经验总结
说了这么多,我总结几条实战中特别有用的经验:
- 能用下标就别炫技:团队协作时,
arr[i]比*(p+i)更容易理解。除非你确定性能瓶颈在这里,否则别为了「显得高级」而牺牲可读性。 - 指针自增要小心:
*p++和(*p)++是完全不同的东西。前者是「取当前值,指针后移」,后者是「取当前值,把值加1」。运算符优先级决定了它们的行为。 - 数组作为函数参数时,会退化为指针:
void func(int arr[])和void func(int *arr)在编译器看来是一回事。所以在函数内部用sizeof(arr)得到的是指针大小,不是数组大小。这个坑我见过无数次了。
一个小技巧:如果你需要在函数内部知道数组长度,要么把长度作为参数传进去,要么用宏定义。我个人习惯用 #define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0])) 这个宏,但注意它只能在数组定义的作用域内使用。
好了,这一章的内容就到这里。指针和数组的关系,说白了就是「地址」和「连续内存块」的关系。搞懂了这一点,后面的二维数组、指针数组、函数指针什么的,都会轻松很多。
公众号:蓝海资料掘金营,微信deep3321