第4章:指针的算术运算
指针这东西,很多人一听到「运算」两个字就头大。其实没那么玄乎。指针的加减,说白了就是地址的移动。我刚开始学的时候也犯过迷糊,总觉得指针加1,地址就加1个字节——后来调试了一个数组越界的bug,才彻底搞明白这里面的门道。
4.1 指针的加减运算
指针加1,到底加了多少?
答案是:取决于指针的类型。
举个例子:
int a = 10;
int *p = &a;
printf("p = %p\n", p);
printf("p+1 = %p\n", p+1);
如果p的地址是0x1000,那么p+1就是0x1004。为什么是4?因为int占4个字节。指针加1,实际上是加上sizeof(所指向的类型)个字节。
核心公式:
指针 + n = 当前地址 + n × sizeof(指针指向的类型)
同理,指针减n就是往低地址方向移动。
int arr[3] = {10, 20, 30};
int *p = &arr[2]; // 指向最后一个元素
p--; // 现在指向 arr[1]
printf("%d\n", *p); // 输出 20
我在项目中遇到过一个问题:有人用char指针去遍历int数组,结果每次加1只跳1个字节,整个逻辑全乱套了。嗯,这就是类型不匹配的典型坑。
4.2 指针与数组的关系
数组名,其实就是个指针常量。它指向数组的第一个元素。
int arr[4] = {1, 2, 3, 4};
// 下面三种写法是等价的
printf("%d\n", arr[0]);
printf("%d\n", *arr);
printf("%d\n", *(arr + 0));
你想想看,arr[2] 和 *(arr+2) 本质上是一回事。编译器会把 arr[i] 翻译成 *(arr + i)。这就是指针运算在数组访问中的底层逻辑。
个人习惯:我写嵌入式代码时,更喜欢用指针方式遍历数组,尤其在做缓冲区处理时。因为指针自增比下标索引在某些编译器上效率更高。当然,现代编译器优化已经很好了,但底层思维不能丢。
来看一个完整的例子:
#include <stdio.h>
int main() {
int arr[] = {10, 20, 30, 40, 50};
int *p = arr; // p 指向 arr[0]
int i;
// 用指针遍历数组
for(i = 0; i < 5; i++) {
printf("arr[%d] = %d, 地址: %p\n", i, *(p + i), p + i);
}
// 指针自增遍历
p = arr;
while(p < arr + 5) {
printf("%d ", *p);
p++;
}
printf("\n");
return 0;
}
注意:数组名是常量,不能做自增操作。arr++ 是错的。但你可以把数组名赋给一个指针变量,然后操作那个指针。
我曾经踩过的坑:在函数里传数组参数时,以为 sizeof(arr) 能拿到整个数组的大小。结果拿到的只是一个指针的大小(4或8字节)。数组作为函数参数时,会退化为指针。这个坑我至少帮三个同事排查过。
4.3 指针比较
指针也能比较大小。比较的是地址的高低。
int arr[3] = {1, 2, 3};
int *p1 = &arr[0];
int *p2 = &arr[2];
if(p1 < p2) {
printf("p1 在低地址\n");
}
if(p1 == &arr[0]) {
printf("p1 指向第一个元素\n");
}
指针比较常用在以下场景:
- 判断是否指向同一个对象:用 == 或 !=
- 判断遍历是否结束:用 < 或 > 判断是否越界
- 判断指针是否为空:用 if(p == NULL) 或 if(!p)
这里有个细节:只有指向同一个数组(或同一块连续内存)的指针才能用 < > 比较。指向不同数组的指针比较大小,行为是未定义的。说白了,编译器不保证结果有意义。
避坑指南:
我曾经在环形缓冲区代码里,用两个指针(头指针和尾指针)做比较来判断缓冲区是空还是满。结果因为指针回绕(wrap around)导致比较逻辑出错。后来改用索引加模运算才解决。指针比较虽然方便,但遇到环形结构时要格外小心。
4.4 指针相减
两个指针相减,得到的是它们之间相差的元素个数,不是字节数。
int arr[5] = {0};
int *p1 = &arr[1];
int *p2 = &arr[4];
int diff = p2 - p1; // 结果是 3,不是 12
printf("相差 %d 个元素\n", diff);
这个特性在计算数组长度时很有用:
int arr[] = {1,2,3,4,5};
int len = &arr[4] - &arr[0] + 1; // 结果是 5
当然,更常见的写法是 sizeof(arr)/sizeof(arr[0])。但指针相减的方式在某些动态场景下更灵活。
4.5 知识体系总览
下面这张图,把指针运算的核心逻辑串起来了:
4.6 小结
指针的算术运算,说白了就是三件事:
- 加减:按类型大小移动地址
- 数组访问:下标就是指针偏移的语法糖
- 比较与相减:判断位置关系,计算元素间距
这些规则在嵌入式开发中天天用。比如操作寄存器地址、遍历缓冲区、管理内存池,都离不开指针运算。我个人建议你多写几个小demo,把指针的地址值打印出来看看,比死记硬背强一百倍。
最后说一句:指针运算的坑,十有八九出在类型不匹配和越界访问上。写代码时多问自己一句:「这个指针加1,到底跳了多少字节?」养成这个习惯,能省下大把调试时间。
公众号:蓝海资料掘金营,微信deep3321