2. 指针与数组
数组和指针,这俩东西在C/C++里就像一对双胞胎。长得像,但性格完全不同。很多新手甚至老手都在这上面栽过跟头。我刚开始工作那会儿,就因为在数组名上理解不到位,线上排查了一整天才找到bug——嗯,说起来都是泪。
数组名到底是什么?
先问个问题:int arr[5] 里的 arr 是什么?
很多人脱口而出:「数组名就是首地址啊!」
对,但不全对。准确地说,数组名在大多数表达式中会被隐式转换为指向首元素的指针。但有两个例外:
- sizeof(arr) —— 返回整个数组的大小,不是指针大小
- &arr —— 取的是整个数组的地址,类型是
int (*)[5]
核心区别:
arr等价于&arr[0],类型是int*&arr类型是int (*)[5],指向整个数组- 虽然数值相同,但指针类型不同,加减运算的结果也不同
#include <stdio.h>
int main() {
int arr[5] = {1, 2, 3, 4, 5};
printf("arr = %p\n", arr); // 首元素地址
printf("&arr[0] = %p\n", &arr[0]); // 同上
printf("&arr = %p\n", &arr); // 整个数组地址
printf("arr + 1 = %p\n", arr + 1); // 偏移4字节
printf("&arr + 1 = %p\n", &arr + 1); // 偏移20字节
return 0;
}
你看,arr + 1 跳过一个int,&arr + 1 跳过整个数组。数值上差了多少?16字节。这就是类型带来的差异。
我的习惯:写代码时尽量用 arr + i 或 &arr[i] 来取元素地址,少用 &arr + i。后者太容易搞混了。
指针运算与数组遍历
数组遍历,说白了就是「从首地址开始,按步长移动指针」。C/C++的指针运算天生就是为数组服务的。
三种常见的遍历方式:
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;
// 方式1:下标法
for (int i = 0; i < 5; i++) {
printf("%d ", arr[i]);
}
// 方式2:指针偏移
for (int i = 0; i < 5; i++) {
printf("%d ", *(p + i));
}
// 方式3:指针递增(我最常用的)
for (int *q = arr; q < arr + 5; q++) {
printf("%d ", *q);
}
我个人偏爱第三种。为什么?因为 q++ 直接移动指针,不需要每次计算偏移量。编译器优化后效率也更高。不过要注意——循环条件一定要用 q < arr + 5,而不是 q <= &arr[4]。后者虽然也能跑,但语义上不够清晰。
避坑指南:我曾经在嵌入式项目里用 while (p <= &arr[4]) 遍历数组,结果数组长度改成动态分配后,&arr[4] 变成了野地址。从那以后,我统一用 起始指针 + 长度 的方式写循环条件。
数组越界的典型场景
数组越界,C/C++程序员的噩梦。它不像Java会抛异常,而是静悄悄地破坏内存。你想想看,一个越界写操作,可能把函数返回地址、其他变量的值、甚至堆管理结构都给覆盖了。
我总结了几种最常见的越界场景:
| 场景 | 代码示例 | 后果 |
|---|---|---|
| 循环边界错误 | for (i = 0; i <= 5; i++) arr[i] = 0; |
写arr[5],越界1个元素 |
| 字符串操作 | strcpy(buf, long_string) |
缓冲区溢出 |
| 指针运算失误 | *(p + offset) 中offset过大 |
访问未知内存 |
| 多维数组遍历 | 内层循环列数写错 | 跨行越界 |
来看一个典型的「差一错误」:
#include <stdio.h>
int main() {
int arr[5] = {0};
int i;
// 错误:i <= 5 导致越界
for (i = 0; i <= 5; i++) {
arr[i] = i * 10;
}
// 打印看看
for (i = 0; i < 5; i++) {
printf("%d ", arr[i]);
}
return 0;
}
这段代码在大部分机器上能「正常」运行,但arr[5]写的是栈上的其他数据。你想想看,如果arr[5]恰好是某个函数指针或者循环变量i本身——那调试起来就酸爽了。
越界的隐蔽性:
- 越界1个字节,可能毫无症状
- 越界覆盖了关键数据,程序在别处崩溃
- 越界写堆内存,释放时crash
- 越界读,可能读到敏感数据
如何定位越界问题?
说实话,越界bug是最难调试的一类。我分享几个实战经验:
- AddressSanitizer(ASan) —— 编译时加
-fsanitize=address,运行时自动检测越界。我项目里默认开启。 - Valgrind —— 适合Linux环境,能检测越界和内存泄漏。
- 断言检查 —— 在关键数组操作前后加assert,比如
assert(index >= 0 && index < size)。 - 日志打印 —— 在怀疑的越界点打印指针值和索引,对比预期范围。
我的小技巧:在调试阶段,我会在数组前后各加一个「哨兵」变量,初始化为固定魔数(比如0xDEADBEEF)。如果程序崩溃时发现哨兵被改了,那100%是越界了。
知识体系图
下面这张图帮你理清本章的核心逻辑:
这张图把本章内容串起来了。从数组名的本质出发,到指针运算的三种遍历方式,再到越界的典型场景,最后落到定位方法上。你写代码时脑子里过一遍这个图,很多坑就能提前避开。
最后提醒一句:别以为越界只发生在数组上。指针运算、字符串操作、memcpy/memset这些函数,只要长度参数算错了,都是越界的重灾区。写代码时多问自己一句:「这个指针指向的内存,真的够用吗?」
可可精品资料库