指针不是数组,数组也不是指针。它们之所以看起来像,是因为在大多数表达式里数组会「退化」成指向首元素的指针——但退化是一种隐式转换,不是等价。

区别一:本质不同
数组是一块连续的对象,指针是一个存放地址的变量。
int arr[5] = {1, 2, 3, 4, 5}; // 分配 5 个 int 的连续空间
int *p = arr; // p 是一个变量,存放 arr[0] 的地址
// 数组名代表这块内存本身
// 指针代表一个可以改变内容的地址值
这个区别的直接后果是:数组没有自己的存储位置来存「地址」,而指针有。
区别二:sizeof 的结果完全不同
对数组用 sizeof 得到整个数组的字节数,对指针用 sizeof 得到地址本身的宽度。
int arr[5];
int *p = arr;
printf("%zun", sizeof(arr)); // 20(5 × 4 字节,64 位平台上 int 通常 4 字节)
printf("%zun", sizeof(p)); // 8(64 位平台上的指针宽度)
这是最实用的一个区别:函数里想求数组长度,不能用 sizeof——参数传进来的是指针。
// 错误:这里 n 永远是 8(指针宽度),不是数组长度
void wrong(int arr[]) {
size_t n = sizeof(arr) / sizeof(arr[0]); // ❌
}
// 正确:长度由调用方传入
void right(int arr[], size_t n) { /* ... */ }
right(arr, sizeof(arr) / sizeof(arr[0])); // ✅ 在定义所在作用域里求
区别三:可赋值性
数组名不是左值,不能重新赋值;指针变量可以随时改指向。
int a[3], b[3];
int *p = a;
p = b; // ✅ 指针改指向
a = b; // ❌ 编译错误:数组名不可赋值
a++; // ❌ 编译错误:数组名不是可修改的左值
p++; // ✅ 指针可以移动
所以「数组之间不能直接赋值」——要用 memcpy 或循环逐元素复制。
区别四:退化(decay)
在大多数表达式里,数组名会隐式转换成指向首元素的指针——这叫退化,是理解两者关系的核心。
int arr[5];
void f(int *p); // 参数写成指针
f(arr); // ✅ arr 退化为 &arr[0]
// 不退化的三种例外:
sizeof(arr) // 数组长度
&arr // 指向整个数组的指针(类型不同,见区别五)
_Alignof(int[5]) // 对齐要求
函数的数组参数 int arr[] 与 int *arr 完全等价——编译器会把前者改写成后者。这就是为什么函数内拿不到数组长度。
区别五:&arr 与 arr 值相同但类型不同
对数组取地址得到「指向整个数组的指针」,值与 arr 相同,但类型和步长不同。
int arr[5];
printf("%pn", (void*)arr); // 首元素地址
printf("%pn", (void*)&arr); // 同样是首元素地址,值相同
// 但类型不同:
// arr 的类型是 int[5],退化为 int*
// &arr 的类型是 int (*)[5] —— 指向「5 个 int 的数组」的指针
printf("%zun", sizeof(*arr)); // 4 (*arr 是 int)
printf("%zun", sizeof(*&arr)); // 20 (*&arr 是整个数组)
// 步长也不同:
printf("%tdn", (char*)(arr + 1) - (char*)arr); // 4 (前进 1 个 int)
printf("%tdn", (char*)(&arr + 1) - (char*)&arr); // 20 (前进整个数组)
这个区别在二维数组传参时特别重要:int (*p)[5] 与 int *p 完全不是一回事。
区别六:字符串字面量的两种写法
这两种写法看起来一样,实际一个是数组(可修改),一个是指针(指向只读区)。
char s1[] = "hello"; // 在栈上分配 6 字节并复制内容 → 可修改
char *s2 = "hello"; // s2 指向只读区的字面量 → 修改是未定义行为
s1[0] = 'H'; // ✅ 没问题
s2[0] = 'H'; // ❌ 未定义行为,通常段错误
实践中一律用 const char * 表示「指向字符串字面量」,让编译器帮你拦住误写。
常见误判
- 「数组就是指针」——错。退化让它们用起来像,但 sizeof、赋值、取地址都不同。
- 「函数里 sizeof(arr) 能得到数组大小」——错,参数已退化为指针。
- 「&arr 和 arr 完全一样」——值一样,类型和步长不一样。
- 「char *s = “abc” 和 char s[] = “abc” 等价」——错,前者只读。
- 「数组名可以 ++」——错,不是左值。
常见问题
为什么函数的数组参数拿不到长度?
因为参数声明 int arr[] 会被编译器改写成 int *arr,数组退化成了指针。指针不知道自己指向的数组有多长,所以长度必须由调用方另行传入。C++ 里可以用 std::array 或 std::span 避开这个问题。
二维数组怎么传参?
两种正确写法:void f(int arr[][5]) 或 void f(int (*arr)[5])。注意第二维长度必须给出,因为编译器要据此计算行偏移。void f(int **arr) 是错的——它表示「指向指针的指针」,与二维数组不是一回事。
指针数组和数组指针有什么区别?
int *a[5] 是指针数组——5 个元素,每个都是 int 指针。int (*a)[5] 是数组指针——一个指针,指向含有 5 个 int 的数组。判断技巧:看括号,先结合谁就是什么。
为什么 sizeof(arr) 在函数外是 20,在函数内是 8?
因为函数外的 arr 是数组,sizeof 得到整个数组大小;函数内的「arr」其实是退化的指针,sizeof 得到指针宽度。这也是为什么「在函数内求数组长度」这个经典错误几乎人人踩过一次。
数组越界为什么有时候不报错?
C 不检查越界。越界写可能破坏相邻变量或堆的元数据,崩溃发生在之后的某个时刻,甚至换个编译器就复现不了。用 -fsanitize=address 或 Valgrind 才能可靠发现。
想进一步理解这些概念?
指针与内存是 C 语言里最容易卡住的一环。如果你在实际代码里遇到困惑——段错误在哪、为什么这个指针失效了——可以看看我们的学习辅导:一对一讲解,用你自己的代码作为例子。
