指针的算术运算:加减整数、相减与数组关系

指针的算术运算,说白了就是让指针在内存里「走路」。

很多新手觉得这很简单,不就是加加减减嘛。但我见过太多人在这上面栽跟头——数组越界、段错误、数据错乱,追查起来让人头皮发麻。我自己刚入行时也踩过类似的坑,调试了一整天才发现是指针多走了一步。

指针加减整数:步长是关键

指针加1,不是地址值加1。这个道理我反复跟团队新人强调。

指针加n,实际地址增加的是 n * sizeof(指向的类型)。这叫「步长」。

int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;          // p 指向 arr[0]

printf("p   = %p\n", p);       // 假设 0x1000
printf("p+1 = %p\n", p+1);     // 0x1004 (int占4字节)
printf("p+2 = %p\n", p+2);     // 0x1008

你看,地址跳了4个字节,不是1个。这就是步长的作用。

核心要点:

  • 指针 + n → 地址增加 n × sizeof(类型)
  • 指针 - n → 地址减少 n × sizeof(类型)
  • 步长由指针的类型决定,不是由指针本身决定

我遇到过一种情况:有人把 char* 强转成 int*,然后做加减运算。结果地址跳了4个字节,但实际数据只有1个字节有效。这种问题最难查,因为编译不报错,运行也不一定崩溃,就是数据不对。

⚠ 警告:

指针加减整数时,必须确保结果仍然指向同一数组对象或其后一个位置。超出这个范围,行为未定义。

我曾经调试过一个bug:某模块用指针遍历数组,循环条件写成了 p <= &arr[5]。arr[5] 根本不存在!虽然大多数情况下能「侥幸」运行,但换了个编译器就崩了。

指针相减:得到的是元素个数

两个指针相减,结果不是地址差,而是「元素个数差」。

int arr[5] = {1, 2, 3, 4, 5};
int *p1 = &arr[1];   // 指向 2
int *p2 = &arr[4];   // 指向 5

ptrdiff_t diff = p2 - p1;   // 结果是 3,不是 12
printf("相差 %td 个元素\n", diff);  // 输出 3

为什么是3?因为 p2 和 p1 之间隔了 arr[2]、arr[3]、arr[4] 三个元素。

这里有个细节:ptrdiff_t 是有符号整数类型,定义在 <stddef.h> 中。我建议你用它来接收指针相减的结果,别用 int。为什么?因为指针差值可能超过 int 的范围,尤其是在64位系统上处理大数组时。

💡 实用技巧:

指针相减常用于计算数组中的索引位置:

int idx = p - arr;   // 得到 p 指向的元素下标

这个技巧我在写二分查找、环形缓冲区时经常用,比维护一个计数器更可靠。

指针与数组:名字就是地址

数组名在大多数表达式中会被隐式转换为指向首元素的指针。这是C语言最基础也最容易混淆的地方。

int arr[5] = {1, 2, 3, 4, 5};

// 以下三种写法等价
int *p1 = arr;       // 数组名隐式转换
int *p2 = &arr[0];   // 取首元素地址
int *p3 = &arr;      // 取整个数组的地址(类型不同!)

等等,&arr 和 arr 地址值一样,但类型不同。这有什么区别?

printf("arr   = %p\n", arr);     // 0x1000
printf("&arr  = %p\n", &arr);    // 0x1000
printf("arr+1 = %p\n", arr+1);   // 0x1004 (跳4字节)
printf("&arr+1= %p\n", &arr+1);  // 0x1014 (跳20字节)

看到了吗?arr+1 跳过一个 int,&arr+1 跳过整个数组。因为 &arr 的类型是 int (*)[5],步长是 5×4=20 字节。

记住这个区别:

  • arr → 类型 int*,指向 arr[0]
  • &arr → 类型 int (*)[5],指向整个数组
  • 地址值相同,但步长不同

我见过有人用 &arr+1 来获取数组末尾地址,然后做遍历。这种做法其实很危险,因为 &arr+1 指向的是数组之后的内存,虽然标准允许指向「后一个位置」,但很多人会误用它来访问数据。

常见越界场景与调试方法

指针越界,说白了就是走到了不该去的地方。我总结了几种典型场景:

场景 代码示例 后果
循环多走一步 for(i=0; i<=5; i++) p[i] 访问 arr[5],越界
指针减法方向错误 p = &arr[4]; p -= 5; 指向数组之前,未定义
类型强转后运算 (int*)((char*)p + 1) 对齐问题,可能崩溃
数组退化为指针后丢失长度 void func(int *p) { p[10] } 无法检查边界

⚠ 我曾经踩过的坑:

有一次我写一个字符串处理函数,用 while(*p++) 遍历字符串。循环结束后,p 已经指向了字符串末尾的 '\0' 之后的位置。然后我直接用 p 去计算长度,结果多算了1。这种 off-by-one 错误在字符串处理中特别常见。

调试技巧:用地址值验证

当你怀疑指针越界时,我建议你这样做:

  1. 打印指针的地址值和目标数组的地址范围
  2. 手动计算指针偏移后的地址是否在合法范围内
  3. 使用 AddressSanitizer(-fsanitize=address)编译,它能自动检测越界
// 调试示例
int arr[5] = {0};
int *p = arr;

printf("数组范围: [%p, %p]\n", arr, &arr[4]);
printf("当前指针: %p\n", p);
printf("偏移后:   %p\n", p + 10);  // 明显越界

// 手动检查
if (p + 10 < arr || p + 10 > &arr[4]) {
    fprintf(stderr, "越界访问!\n");
}

嗯,这个方法虽然土,但在嵌入式环境或者没有 AddressSanitizer 的平台上,它就是救命稻草。

知识体系总览

下面这张图是我整理的指针算术运算核心逻辑,你可以对照着梳理思路:

指针算术运算知识体系 指针算术运算 指针 ± 整数 地址变化 = n × sizeof(类型) 步长由指针类型决定 必须指向同一数组或后一位置 指针 - 指针 结果 = 元素个数差 类型:ptrdiff_t 两个指针必须指向同一数组 数组名与指针 arr 等价于 &arr[0] &arr 步长为整个数组 数组名不是左值 核心原则:指针运算必须保证结果在合法范围内 越界访问 = 未定义行为 = 调试噩梦

这张图把指针算术运算的三个核心方向都串起来了。你写代码时,只要对照着检查指针的「步长」和「范围」,就能避免大部分越界问题。

💡 我的个人习惯:

每次写指针遍历的循环,我都会在注释里标出数组的起始地址和结束地址。比如:

// arr[0..4] 范围: [0x1000, 0x1010]
for (int *p = arr; p <= &arr[4]; p++) {
    // ...
}

这个习惯帮我省了不少调试时间。你想想看,如果代码里到处都是裸指针,没有边界信息,出问题了只能靠猜。

指针算术运算其实不复杂,但细节决定成败。步长、类型、边界,这三个词你记住了,指针越界就能避开一大半。