指针与数组:指针运算、数组名与指针、指针数组
指针和数组,这两个概念在C++里就像一对双胞胎。长得像,但性格完全不同。很多初学者容易搞混,说实话,我当年也在这上面栽过跟头。
今天咱们就把这层窗户纸捅破。我会结合自己项目里的实际经验,把指针运算、数组名到底是不是指针、以及指针数组这几个核心问题讲清楚。
1. 数组名:它到底是不是指针?
很多人刚学的时候,老师会说「数组名就是指针」。嗯,这话对了一半,但另一半会坑死人。
先看个例子:
int arr[5] = {10, 20, 30, 40, 50};
int* p = arr; // 这行能编译通过
cout << sizeof(arr) << endl; // 输出 20(5个int,每个4字节)
cout << sizeof(p) << endl; // 输出 8(64位系统,指针大小)
看到了吗?sizeof(arr) 是整个数组的大小,而 sizeof(p) 只是指针本身的大小。如果数组名就是指针,那这两个结果应该一样才对。
核心结论:数组名在大多数表达式中会「退化」成指向首元素的指针,但它本身不是指针变量。它是一个常量地址,你不能给它赋值。
我在项目中遇到过一个问题:有人试图用 arr++ 来遍历数组,结果编译报错。为什么?因为 arr 是常量,不能自增。但 p++ 就可以。这就是数组名和指针最本质的区别。
2. 指针运算:别小看 +1 这个操作
指针运算的核心就一句话:指针加1,跳过一个元素的大小。
什么意思?看代码:
int arr[5] = {10, 20, 30, 40, 50};
int* p = arr;
cout << p << endl; // 比如 0x1000
cout << p + 1 << endl; // 0x1004(不是0x1001!)
cout << p + 2 << endl; // 0x1008
因为 int 占4个字节,所以 p+1 实际上加了4个字节。这个规则对任何类型都适用。
我曾经在写一个网络协议解析模块时,需要从缓冲区里按字节读取数据。当时我用 char* 指针来遍历,char 是1个字节,所以 p+1 正好跳到下一个字节。如果我用 int*,那 p+1 就跳4个字节,数据全乱套了。
小技巧:如果你想让指针按字节移动,就用 char* 或 void*。但 void* 不能做算术运算,需要先转成 char*。
3. 指针数组 vs 数组指针:别搞混了
这两个名字长得像,但完全是两码事。我面试别人的时候,经常拿这个来考。
| 概念 | 写法 | 含义 |
|---|---|---|
| 指针数组 | int* arr[5] |
一个数组,里面存了5个 int* 指针 |
| 数组指针 | int (*arr)[5] |
一个指针,指向一个包含5个int的数组 |
怎么记?看优先级:[] 的优先级比 * 高。所以 int* arr[5] 先被解释成「arr是一个数组,里面有5个元素,每个元素是 int*」。而 int (*arr)[5] 加了括号,* 先和 arr 结合,所以「arr是一个指针,指向一个数组」。
指针数组在实际项目中非常常见。比如你要管理多个字符串:
const char* fruits[3] = {"apple", "banana", "cherry"};
for (int i = 0; i < 3; i++) {
cout << fruits[i] << endl; // 每个元素都是一个字符串指针
}
数组指针用得少一些,但在处理二维数组时很有用。比如你要写一个函数,接收一个二维数组:
void printMatrix(int (*matrix)[4], int rows) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < 4; j++) {
cout << matrix[i][j] << " ";
}
cout << endl;
}
}
注意:数组指针的声明很容易写错。少一个括号,意思就全变了。我建议你写的时候先写 (*ptr),再写后面的 [size],这样不容易出错。
4. 指针与数组的遍历:哪种方式更快?
你可能会想,用指针遍历数组是不是比用下标快?
嗯,在早期的编译器中,确实有区别。指针自增比下标计算要快一点点。但现在编译器优化已经很成熟了,两者生成的机器码几乎一样。
我个人习惯用下标,因为可读性更好。但如果你在写底层代码,比如嵌入式系统或驱动,用指针会更灵活。
// 下标方式
for (int i = 0; i < 5; i++) {
cout << arr[i] << " ";
}
// 指针方式
for (int* p = arr; p < arr + 5; p++) {
cout << *p << " ";
}
两种写法结果一样。但指针方式有一个好处:你可以用 p < arr + 5 作为循环条件,这比 i < 5 更直观地表达了「指针还没越界」的意思。
5. 避坑指南:我曾经踩过的雷
我曾经在项目里犯过一个低级错误。当时要清空一个数组,我写了这样一段代码:
int buffer[1024];
int* p = buffer;
// 错误写法
for (int i = 0; i < 1024; i++) {
*p++ = 0; // 这行没问题
}
// 但之后我试图用 buffer 来访问数据,发现 buffer 的地址没变
// 而 p 已经指向了 buffer 的末尾
问题出在哪?p 是一个指针变量,它可以移动。但 buffer 是数组名,它不会移动。所以清空之后,buffer 仍然指向数组开头,而 p 已经跑到了末尾。如果你之后还想用 p 来访问数据,必须先把它重置回 buffer。
教训:用指针遍历数组时,一定要记得保存原始地址。要么用另一个指针变量来遍历,要么在遍历结束后把指针重置回去。
6. 知识体系图
下面这张图帮你理清指针与数组的关系:
7. 总结一下
指针和数组的关系,说白了就这几点:
- 数组名不是指针变量,但会退化成指针。记住
sizeof的区别。 - 指针运算的单位是元素大小,不是字节。想按字节移动就用
char*。 - 指针数组是数组存指针,数组指针是指针指向数组。写法差一个括号,意思差十万八千里。
- 遍历数组时,下标和指针方式性能差不多,选你习惯的就好。
嗯,这些内容够你消化一阵子了。下次遇到指针和数组的问题,先想想今天说的这几个点,应该能少踩不少坑。