指针与数组:指针运算、数组名与指针、指针数组

指针和数组,这两个概念在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. 知识体系图

下面这张图帮你理清指针与数组的关系:

指针与数组 数组名 退化为首元素指针 sizeof(arr) ≠ sizeof(ptr) 常量地址 不能 arr++ 指针运算 p + 1 跳过一个元素 int* 加1 → 地址+4 指针数组 int* arr[5] 数组存指针,常用于字符串数组 数组指针 int (*arr)[5] 指针指向数组,用于二维数组传参

7. 总结一下

指针和数组的关系,说白了就这几点:

  • 数组名不是指针变量,但会退化成指针。记住 sizeof 的区别。
  • 指针运算的单位是元素大小,不是字节。想按字节移动就用 char*
  • 指针数组是数组存指针,数组指针是指针指向数组。写法差一个括号,意思差十万八千里。
  • 遍历数组时,下标和指针方式性能差不多,选你习惯的就好。

嗯,这些内容够你消化一阵子了。下次遇到指针和数组的问题,先想想今天说的这几个点,应该能少踩不少坑。

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