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

下面这张图,把指针算术运算的核心知识点串起来了。我建议你保存下来,写代码时对照着看。

指针算术运算知识体系 指针算术运算 指针 + 整数 指针 - 指针 指针比较 越界风险 跳元素非字节 步长=sizeof(类型) 类型必须匹配 结果=元素个数 类型ptrdiff_t 需同数组内 高地址更大 用于边界判断 不同数组未定义 段错误崩溃 内存污染 隐蔽难排查 核心原则:指针运算必须基于同一内存块,且不能越界访问

3.6 总结

指针算术运算,说白了就是三件事:加减整数、指针相减、指针比较。每一件事都有明确的规则,但规则背后都藏着一个共同的原则——必须在同一内存块内操作

我个人习惯在写指针运算代码时,先问自己三个问题:

  1. 这个指针指向的内存块有多大?
  2. 我做的运算会不会超出这个范围?
  3. 如果超出,程序会怎样?

养成这个习惯,能避免90%的指针问题。剩下的10%,嗯,那就靠调试器和经验了。

我的建议:在嵌入式开发中,开启编译器的地址 sanitizer 或者边界检查功能。虽然会降低一点性能,但能在开发阶段就捕获越界问题,比产品上线后崩溃强得多。


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