第19章:指针与数组——指针访问数组元素,数组名作为指针常量,指针运算
指针和数组,在C语言里就像一对双胞胎。长得像,性格却不同。我做了这么多年嵌入式开发,见过太多人在它们身上栽跟头。今天咱们就把这层窗户纸捅破。
19.1 数组名:披着数组外衣的指针常量
先问一个问题:int arr[5] 里的 arr 到底是什么?
很多人背过「数组名就是首地址」。对,但不全对。准确地说,数组名是一个指针常量——它的值就是数组首元素的地址,而且这个值不能改。
核心区别:
arr等价于&arr[0],类型是int*- 但
arr是常量,不能做arr++或arr = something - 而一个
int* p指针变量,可以随便指向别处
我在项目中遇到过这样一个bug:有人写了 arr++ 想遍历数组,编译器直接报错。他一脸懵:「指针不就能++吗?」嗯,数组名不是普通指针,它是常量。
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr; // 正确:p指向arr[0]
// arr = p; // 错误:arr是常量,不能赋值
// arr++; // 错误:不能修改数组名
p++; // 正确:p是变量,可以移动
19.2 指针访问数组元素:三种写法,一个意思
访问数组第 i 个元素,C语言给了你三种姿势。我个人习惯用下标法,因为可读性最好。但你要知道,它们本质是一样的。
| 写法 | 含义 | 备注 |
|---|---|---|
arr[i] |
下标法 | 最直观,推荐 |
*(arr + i) |
指针法 | 底层本质 |
p[i] |
指针当下标 | p指向数组首元素时可用 |
int arr[4] = {1, 2, 3, 4};
int *p = arr;
// 三种写法,完全等价
printf("%d\n", arr[2]); // 3
printf("%d\n", *(arr + 2)); // 3
printf("%d\n", p[2]); // 3
小技巧: 编译器看到 arr[i] 时,会自动翻译成 *(arr + i)。所以下标操作本质上就是指针运算。你想想看,2[arr] 这种写法居然也能编译通过——因为 2[arr] 等价于 *(2 + arr)。虽然没人这么写,但能帮你理解本质。
19.3 指针运算:加减的学问
指针加减整数,不是简单的地址加减。它是以「元素大小」为步长的。说白了,p + 1 跳过一个元素,而不是跳过一个字节。
int arr[3] = {100, 200, 300};
int *p = arr;
printf("p = %p\n", p); // 假设 0x1000
printf("p+1 = %p\n", p + 1); // 0x1004(int占4字节)
printf("p+2 = %p\n", p + 2); // 0x1008
为什么会这样?因为指针的类型决定了步长。int* 每次加4字节,char* 每次加1字节,double* 每次加8字节。我在调试一个通信协议时,就因为这个特性踩过坑——用 char* 遍历结构体数组,步长算错了,数据全对不上。
避坑指南: 我曾经在STM32上写DMA传输,用指针遍历一个 uint16_t 数组。结果不小心把指针声明成了 uint8_t*,每次加1只移动了1字节,数组后半段数据全读错了。排查了整整一下午。记住:指针的类型决定了它移动的步长。
19.4 指针相减:得到的是元素个数
两个同类型的指针相减,结果不是地址差,而是元素个数。这个特性在计算数组长度时特别有用。
int arr[8] = {0};
int *start = &arr[2];
int *end = &arr[6];
int count = end - start; // 结果是4,不是16
printf("元素个数: %d\n", count); // 4
嗯,这里要注意:指针相减要求两个指针指向同一个数组(或者同一个对象之后的下一个位置),否则结果是未定义的。别拿两个不同数组的指针去减,那是自找麻烦。
19.5 数组名 vs &数组名:一个经典陷阱
这个问题我面试时经常问,能答对的人不多。
int arr[5] = {0};
printf("arr = %p\n", arr); // 首元素地址
printf("&arr[0] = %p\n", &arr[0]); // 同上
printf("&arr = %p\n", &arr); // 整个数组的地址
// 数值上相同,但类型不同
printf("arr+1 = %p\n", arr + 1); // 跳过一个int
printf("&arr+1 = %p\n", &arr + 1); // 跳过整个数组(5个int)
arr 的类型是 int*,指向一个int元素。&arr 的类型是 int (*)[5],指向一个包含5个int的数组。虽然它们打印出来的地址值一样,但加1之后,跨度完全不同。
记住这个规律:
arr + 1→ 移动sizeof(int)字节&arr + 1→ 移动sizeof(arr)字节,即5 * sizeof(int)
19.6 知识体系图
下面这张图帮你理清本章的核心逻辑。建议保存下来,写代码时对照着看。
19.7 实战:用指针遍历二维数组
二维数组在内存中是连续存储的。你可以用一个一级指针从头到尾遍历所有元素。我在做图像处理时经常这么干——把二维像素数组当一维处理,效率更高。
int matrix[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
int *p = &matrix[0][0]; // 或者直接用 matrix[0]
for (int i = 0; i < 6; i++) {
printf("%d ", *(p + i)); // 1 2 3 4 5 6
}
个人习惯: 遍历二维数组时,我更喜欢用 int *p = &matrix[0][0] 这种写法,虽然啰嗦一点,但意图明确。你想想看,如果写成 int *p = matrix,编译器会报警告——因为 matrix 的类型是 int (*)[3],不是 int*。虽然强制转换也能用,但写清楚更安全。
19.8 本章小结
- 数组名是指针常量,不能自增自减,也不能被赋值
- 访问元素有下标法和指针法,本质相同
- 指针加减以元素大小为步长,类型决定步长
- 指针相减得到元素个数,不是字节数
- &数组名得到整个数组的地址,类型是数组指针
指针和数组这块,说白了就是「类型决定行为」。搞懂了类型,你就搞懂了一切。我当年刚学C时,也是被这些绕得晕头转向,后来在项目里踩了几次坑,才真正理解。别急,多写多练,自然就通了。
公众号:蓝海资料掘金营,微信 deep3321