3、指针的算术运算:指针加减整数、指针相减、指针比较、指针越界风险
指针的算术运算,是C语言里最让人又爱又恨的特性之一。爱它,是因为操作数组时真的方便;恨它,是因为稍不留神就踩坑。我做了十几年嵌入式开发,见过太多因为指针运算翻车的案例——有的直接导致系统崩溃,有的则是隐蔽的内存污染,排查起来让人抓狂。
说白了,指针的加减不是简单的地址加减。它背后有一套严格的规则,理解透了才能用好。
3.1 指针加减整数:跳的是元素,不是字节
先看一个最基础的操作:ptr + n。很多人以为这就是地址加n个字节,其实不是。指针加整数,移动的是元素,不是字节。
举个例子:
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr; // p指向arr[0]
int *q = p + 3; // q指向arr[3]
printf("%d\n", *q); // 输出40
这里p + 3,地址实际增加了 3 * sizeof(int) 个字节。如果int是4字节,那就是跳了12字节。编译器会自动根据指针类型计算步长。
核心公式:
ptr + n 的实际地址 = 原地址 + n × sizeof(指针指向的类型)
我在项目中遇到过一个问题:有人用char *指向一个int数组,然后做加法。结果地址只跳了n个字节,而不是n个int。嗯,这种类型不匹配的指针运算,很容易出问题。
3.2 指针相减:得到的是元素个数
两个同类型的指针相减,结果不是字节差,而是元素个数差。这个特性在数组操作中非常实用。
int arr[5] = {10, 20, 30, 40, 50};
int *p = &arr[1]; // 指向20
int *q = &arr[4]; // 指向50
ptrdiff_t diff = q - p; // 结果是3,不是12
printf("相差 %ld 个元素\n", diff); // 输出3
你想想看,如果两个指针指向同一个数组的不同位置,相减就能知道它们之间隔了多少个元素。这在实现一些算法时特别好用。
小提示:指针相减的结果类型是 ptrdiff_t,定义在 <stddef.h> 中。别直接用int存,因为int可能不够大。我习惯用 ptrdiff_t,安全。
注意:只有指向同一个数组(或同一个对象之后的下一个位置)的指针才能相减。指向不同数组的指针相减,行为是未定义的。我曾经见过有人拿两个不同malloc出来的指针相减,结果算出一个莫名其妙的值——这种代码,说它错吧,它还能跑;说它对,逻辑上完全站不住脚。
3.3 指针比较:谁大谁小有讲究
指针也能比较大小。规则很简单:指向高地址的指针更大。但前提是——两个指针必须指向同一个数组(或同一个对象)的范围内。
int arr[5] = {10, 20, 30, 40, 50};
int *p = &arr[0];
int *q = &arr[3];
if (p < q) {
printf("p在q的前面\n"); // 会执行
}
指针比较常用在遍历数组时判断边界:
int arr[10];
int *start = arr;
int *end = arr + 10; // 指向数组末尾的下一个位置
while (start < end) {
// 处理 *start
start++;
}
警告:指向不同数组的指针不能比较大小。比如一个指向全局数组,一个指向栈上的数组,比较结果是未定义的。编译器可能给你一个警告,也可能不警告,但结果不可预测。
为什么会这样?因为C语言标准没有规定不同对象在内存中的相对位置。编译器可以自由安排,你没法假设哪个地址更高。
3.4 指针越界风险:最隐蔽的杀手
指针越界,是C语言中最常见也最危险的问题之一。我这么说吧,我职业生涯中遇到的最难排查的bug,十有八九跟指针越界有关。
看这个例子:
int arr[3] = {1, 2, 3};
int *p = arr;
*(p + 3) = 100; // 越界!arr只有3个元素,索引0-2
这段代码能编译通过,甚至可能正常运行。但它在arr[3]的位置写入了100,而这个位置属于别的变量或者内存管理区域。后果可能是:
- 程序立即崩溃(段错误)
- 某个不相关的变量被悄悄修改
- 函数返回地址被破坏,导致奇怪的跳转
- 堆内存管理结构被破坏,后续malloc/free崩溃
我曾经在一个通信协议栈项目里,遇到一个间歇性死机的问题。查了整整三天,最后发现是一个指针在循环里多跳了一步,写到了相邻的结构体成员上。那个结构体里存的是缓冲区长度,被改成了0,导致后续所有数据都写不进去。嗯,从那以后,我对指针边界检查格外敏感。
避坑指南:
- 数组下标从0开始,最大下标是size-1
- 指针可以指向数组末尾的下一个位置(用于比较),但不能解引用
- malloc出来的内存,同样有越界风险,别以为堆内存就安全
- 字符串操作尤其容易越界,strcpy、sprintf都是重灾区
3.5 知识体系总览
下面这张图,把指针算术运算的核心知识点串起来了。我建议你保存下来,写代码时对照着看。
3.6 总结
指针算术运算,说白了就是三件事:加减整数、指针相减、指针比较。每一件事都有明确的规则,但规则背后都藏着一个共同的原则——必须在同一内存块内操作。
我个人习惯在写指针运算代码时,先问自己三个问题:
- 这个指针指向的内存块有多大?
- 我做的运算会不会超出这个范围?
- 如果超出,程序会怎样?
养成这个习惯,能避免90%的指针问题。剩下的10%,嗯,那就靠调试器和经验了。
我的建议:在嵌入式开发中,开启编译器的地址 sanitizer 或者边界检查功能。虽然会降低一点性能,但能在开发阶段就捕获越界问题,比产品上线后崩溃强得多。
公众号:蓝海资料掘金营,微信deep3321