第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 知识体系总览

下面这张图,把指针运算的核心逻辑串起来了:

指针算术运算知识体系 指针算术运算 指针加减运算 地址移动 = n × sizeof(类型) p++ / p-- 移动一个元素 指针与数组 arr[i] ≡ *(arr + i) 数组名是常量指针 指针比较 == / != / < / > 仅限同一内存区域 指针相减 结果 = 元素个数 常用于计算数组长度 核心:一切运算都基于「指针类型」决定步长

4.6 小结

指针的算术运算,说白了就是三件事:

  1. 加减:按类型大小移动地址
  2. 数组访问:下标就是指针偏移的语法糖
  3. 比较与相减:判断位置关系,计算元素间距

这些规则在嵌入式开发中天天用。比如操作寄存器地址、遍历缓冲区、管理内存池,都离不开指针运算。我个人建议你多写几个小demo,把指针的地址值打印出来看看,比死记硬背强一百倍。

最后说一句:指针运算的坑,十有八九出在类型不匹配和越界访问上。写代码时多问自己一句:「这个指针加1,到底跳了多少字节?」养成这个习惯,能省下大把调试时间。


公众号:蓝海资料掘金营,微信deep3321