指针越界排查与修复实战 - 第2节
指针的算术运算:加减整数
指针能加减整数,这是C/C++里最基础也最容易踩坑的地方。
我刚开始学的时候,总觉得指针加减就是地址的加减。比如 int* p,p+1 不就是地址加1吗?
错。
实际上,p+1 加的是 sizeof(int) 个字节。在我的机器上,int 是4字节,所以地址会跳4个字节。
为什么会这样?
因为指针的类型决定了它的步长。说白了,编译器知道 p 指向的是 int,所以 p+1 自然要指向下一个 int 的起始位置。
核心公式:
指针 p 加上整数 n,实际地址 = p + n * sizeof(指针指向的类型)
看个例子:
int arr[] = {10, 20, 30, 40};
int* p = arr; // p 指向 arr[0]
printf("%p\n", p); // 比如 0x1000
printf("%p\n", p+1); // 0x1004,不是 0x1001
printf("%d\n", *(p+2)); // 输出 30
我在项目中遇到过一个问题:有人用 char* 去遍历 int 数组,结果每次只跳1个字节,数据全读错了。嗯,这就是类型不匹配的典型后果。
注意:指针加减整数时,如果指针指向数组之外,就是未定义行为。我曾经调试过一个崩溃,就是因为循环多跑了一次,指针越界了。
指针与数组的关系
数组名和指针,很多人觉得它们是一回事。其实不然。
数组名在大多数表达式中会隐式转换为指向首元素的指针。但数组名本身不是指针变量,它没有自己的存储空间。
你想想看:
int arr[5];
int* p = arr; // 等价于 int* p = &arr[0]
// arr 和 p 在很多场景下可以互换使用
printf("%d\n", arr[2]); // 没问题
printf("%d\n", p[2]); // 也没问题
但有一个关键区别:
printf("%zu\n", sizeof(arr)); // 输出 20 (5个int)
printf("%zu\n", sizeof(p)); // 输出 8 (指针本身的大小,64位系统)
我建议你记住一句话:数组名是常量,指针是变量。你不能写 arr++,但可以写 p++。
个人经验:我习惯用指针遍历数组,而不是用下标。因为指针运算通常更快,而且代码更灵活。但要注意边界检查,别跑出去了。
指针比较运算
指针可以比较大小,前提是它们指向同一个数组(或同一个对象的成员)。
比较规则很简单:
p == q:指向同一个位置p < q:p 指向的地址更小p > q:p 指向的地址更大
看个实际用法:
int arr[5] = {1, 2, 3, 4, 5};
int* start = arr;
int* end = arr + 5; // 指向最后一个元素之后
// 用指针比较来遍历
while (start < end) {
printf("%d ", *start);
start++;
}
// 输出:1 2 3 4 5
这里 end 指向的是数组末尾之后的一个位置,这叫"尾后指针"。C标准允许你取这个地址,但不能解引用它。
我曾经犯过一个错:把两个不同数组的指针拿来比较大小。结果在不同编译器上行为不一致,调试了半天才发现问题。记住,不同数组的指针比较是未定义行为。
指针比较还有一个常见用途:判断指针是否在某个范围内。
int* p = arr + 2;
if (p >= arr && p < arr + 5) {
printf("p 在数组范围内\n");
}
这种写法在写内存池、缓冲区管理时特别有用。我自己的项目中就经常用这种方式做边界检查。
知识体系总览
下面这张图把本章的核心逻辑串起来了:
这张图把三个核心知识点串起来了。你看,指针加减整数、指针与数组的关系、指针比较运算,它们都围绕着一个核心:类型决定步长,边界决定安全。
我个人的习惯是,每次写指针运算前,先问自己三个问题:
- 这个指针的类型是什么?
- 它指向的数组范围是多少?
- 加减后会不会越界?
养成这个习惯,能帮你避开90%的指针越界问题。
避坑指南:我曾经在写一个网络协议解析器时,用指针遍历数据包。因为没注意类型转换后的步长变化,导致解析结果全乱了。后来我加了一行断言检查指针范围,问题就再没出现过。
好了,指针的算术运算就聊到这儿。记住:加减看类型,比较看范围,数组名不是变量。把这些基础打牢,后面讲指针越界排查时你才能游刃有余。
可可精品资料库