C++ 指针 vs 数组(一文讲透)

C++ 指针 vs 数组:初学者必须搞懂的内存秘密

在学习 C++ 的过程中,很多开发者都会遇到一个经典难题:指针和数组到底有什么区别?它们看起来都能访问同一块内存,使用方式也相似,但背后却隐藏着巨大的差异。如果你也曾困惑于“为什么数组名能当指针用”、“为什么动态数组要用 new”、“为什么有时候数组会退化成指针”,那这篇文章就是为你准备的。

我们不讲抽象理论,而是用真实代码和生活中的比喻,带你一步步揭开 C++ 指针与数组之间那层神秘面纱。相信我,理解清楚这一点,你对内存管理、函数传参、动态分配等高级概念的理解会瞬间提升一个台阶。


数组的本质:一块连续的内存区域

在 C++ 中,数组是一种固定大小的连续内存块。当你声明一个数组时,编译器会为你在栈上(或堆上,视情况而定)分配一段连续的内存空间,用于存放指定数量的相同类型数据。

int arr[5] = {10, 20, 30, 40, 50};

这行代码做了什么?

  • 在栈上分配了 5 个 int 类型的空间(通常每个 int 占 4 字节,共 20 字节)
  • 将数值依次填入内存
  • arr 是这个内存块的首地址,但它本身是一个常量,不能修改

📌 关键点:数组名(如 arr)是一个地址常量,它指向数组的起始位置,但不能被赋值为其他地址。

想象一下,数组就像一个固定的书架,有 5 个格子,每个格子放一本书(数据)。书架的名字是固定的,你不能把整个书架移动到另一个地方,但你可以取出书来读或写。


指针的本质:一个存储地址的变量

指针是一个变量,它的值是另一个变量的内存地址。你可以把指针想象成一张地图上的坐标点,它不直接存放数据,而是告诉你“数据在哪儿”。

int value = 100;
int* ptr = &value;  // ptr 存储的是 value 的地址
  • &value 获取变量 value 的内存地址
  • int* ptr 声明一个指向 int 类型的指针变量
  • ptr 是一个变量,可以被重新赋值

📌 关键点:指针是变量,可以改变其指向;数组名是常量,不能改变指向。

这就像你有一张地图,上面写着“超市在 100 号门”,你可以把这张地图复制一份,贴在别的地方,甚至修改它说“超市在 200 号门”。但如果你把“超市”本身当作一个固定地点,那它就不能搬家了。


数组名会退化为指针?真相揭秘

这是 C++ 中最让人迷惑的点之一:数组名在大多数情况下会自动退化为指向首元素的指针

int arr[5] = {10, 20, 30, 40, 50};

// 以下两种写法等价(在函数传参等场景中)
void printArray(int* ptr) {
    for (int i = 0; i < 5; ++i) {
        std::cout << ptr[i] << " ";
    }
}

// 调用时
printArray(arr);  // arr 退化为 &arr[0]

为什么能这样?因为当你把数组名传给函数时,编译器会自动把它当作指针处理。

但注意:这种退化只在表达式中发生。比如你不能对数组名进行 ++ 操作,因为它是常量。

arr++;  // 错误!数组名是常量,不能修改

📌 重要区别:数组名是“常量地址”,指针是“可变地址变量”。


动态数组:指针的真正舞台

当需要创建大小在运行时才确定的数组时,必须使用动态内存分配,这时就完全依赖指针了。

int size;
std::cin >> size;

// 使用 new 动态分配内存
int* dynamicArr = new int[size];

// 使用完毕后必须 delete
delete[] dynamicArr;

这里的关键是:new int[size] 返回的是一个指针,指向堆上分配的连续内存。而 size 是变量,所以数组大小可变。

⚠️ 警告:如果忘记 delete[],就会造成内存泄漏!这是 C++ 开发中常见的陷阱。

相比之下,静态数组的大小必须是编译时常量:

// int arr[size];  // 错误!size 是运行时变量

所以,动态数组 = 指针 + new + delete,这是 C++ 中指针真正发挥作用的场景。


指针 vs 数组:对比总结

下面这张表格帮你快速理清两者的核心差异:

特性 数组 指针
内存位置 栈(静态)或堆(动态) 堆或栈(作为变量)
是否可修改 数组名是常量,不可修改指向 指针是变量,可修改指向
大小获取 使用 sizeof(arr)/sizeof(arr[0]) 无法直接获取大小,需额外记录
传参行为 退化为指针 传入函数时仍是地址
动态分配 不支持(除非用 new) 支持 new[]
内存管理 自动释放(栈)或手动(堆) 必须手动管理(delete)

实际案例:函数传参中的陷阱

考虑下面这个函数:

void modifyArray(int arr[], int size) {
    arr[0] = 999;  // 修改第一个元素
}

int main() {
    int data[3] = {1, 2, 3};
    modifyArray(data, 3);
    std::cout << data[0];  // 输出 999
    return 0;
}

你可能会觉得“数组传进去,函数里修改了,原数组也变了”,这似乎说明数组是“引用传递”。但真相是:arr[] 其实是 int* arr 的语法糖!函数接收到的是一个指针,所以修改的是原始内存。

结论:C++ 中所有数组传参都等价于指针传参,没有真正的数组传递机制

如果你希望函数内部不修改原数组,就必须传值或使用 const 指针:

void safeFunction(const int* arr, int size) {
    // arr[0] = 999;  // 错误!不能修改
}

指针算术:你必须掌握的底层能力

指针支持算术运算,这是 C++ 灵活性的体现。比如:

int arr[5] = {10, 20, 30, 40, 50};
int* ptr = arr;  // 指向 arr[0]

std::cout << *ptr << std::endl;      // 输出 10
std::cout << *(ptr + 1) << std::endl; // 输出 20
std::cout << *(ptr + 3) << std::endl; // 输出 40

ptr + 1 并不是加 1 字节,而是加 sizeof(int) 字节(通常 4 字节),这叫做指针步进

这就像你在走楼梯:每走一步,不是前进 1 米,而是前进“一个房间的长度”。指针的步进单位由它所指向的数据类型决定。


最佳实践:何时用数组,何时用指针?

  • 用数组:当大小已知、固定、且不需要动态分配时。比如配置项、小规模数据集合。
  • 用指针:当需要动态内存、函数传参、复杂数据结构(链表、树、图)时。
  • 强烈建议:优先使用 std::vector 替代原始数组和 new/delete,它自动管理内存,安全又高效。
#include <vector>
std::vector<int> vec = {1, 2, 3, 4, 5};  // 自动管理内存
vec.push_back(6);

结语:理解指针与数组,是通往 C++ 高级编程的第一步

C++ 指针 vs 数组,表面上看是两种语法,实则是内存访问方式的两种表达。数组是“静态容器”,指针是“动态地图”。掌握它们的区别,不仅能写出更高效的代码,更能避免内存泄漏、野指针等致命错误。

不要害怕指针,它只是让你直接操控内存的“钥匙”。当你能熟练运用指针与数组,你就真正进入了 C++ 的核心世界。

记住:指针是工具,数组是容器,理解它们的关系,才能驾驭 C++ 的强大。