指针的算术运算:加减整数、相减与数组关系
指针的算术运算,说白了就是让指针在内存里「走路」。
很多新手觉得这很简单,不就是加加减减嘛。但我见过太多人在这上面栽跟头——数组越界、段错误、数据错乱,追查起来让人头皮发麻。我自己刚入行时也踩过类似的坑,调试了一整天才发现是指针多走了一步。
指针加减整数:步长是关键
指针加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 错误在字符串处理中特别常见。
调试技巧:用地址值验证
当你怀疑指针越界时,我建议你这样做:
- 打印指针的地址值和目标数组的地址范围
- 手动计算指针偏移后的地址是否在合法范围内
- 使用 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 的平台上,它就是救命稻草。
知识体系总览
下面这张图是我整理的指针算术运算核心逻辑,你可以对照着梳理思路:
这张图把指针算术运算的三个核心方向都串起来了。你写代码时,只要对照着检查指针的「步长」和「范围」,就能避免大部分越界问题。
💡 我的个人习惯:
每次写指针遍历的循环,我都会在注释里标出数组的起始地址和结束地址。比如:
// arr[0..4] 范围: [0x1000, 0x1010]
for (int *p = arr; p <= &arr[4]; p++) {
// ...
}
这个习惯帮我省了不少调试时间。你想想看,如果代码里到处都是裸指针,没有边界信息,出问题了只能靠猜。
指针算术运算其实不复杂,但细节决定成败。步长、类型、边界,这三个词你记住了,指针越界就能避开一大半。