C语言开发最容易混淆的知识点 ——数组和指针到底有什么区别?
时间:2026-10-09 来源:华清远见
在C语言的学习和面试中,有一个问题几乎人人都会被问到:“数组和指针到底是不是一回事?”。很多人会脱口而出:“数组名就是指针嘛!”这句话一半对,一半错。对的部分是:在很多表达式场景下,数组名会“退化”成指向首元素的指针。错的部分是:数组是数组,指针是指针,它们是两种完全不同的类型。把两者混为一谈,是C语言开发中最容易踩坑的知识点之一。
下面,本文就用几段简单的示例代码和图解,把这件事讲清楚。
一、先看一段“看起来能用”的代码
#include <stdio.h>
void func(int arr[]) {
printf("sizeof(arr) = %zu\n", sizeof(arr)); // 输出的是指针大小
}
int main(void) {
int a[5] = {1, 2, 3, 4, 5};
printf("sizeof(a) = %zu\n", sizeof(a)); // 输出整个数组大小
func(a);
return 0;
}
在64位机器上,典型的输出是:
sizeof(a) = 20
sizeof(arr) = 8
为什么同一个“数组”,在 main 里是 20 字节,传到函数里就变成 8 字节了?因为 函数参数中的 int arr[] 其实等价于 int *arr,它已经不是数组了,而是一个指针。这就是“数组退化成指针”的经典表现。
二、数组和指针的本质区别
我们从三个角度来对比。
(一). 类型不同
int a[5];
int *p = a;
•a 的类型是 int[5],即“含5个int的数组”。
•p 的类型是 int *,即“指向int的指针”。
它们是不同的类型。sizeof 的结果不同就是最直接的证据:
sizeof(a) == 5 * sizeof(int) == 20;
sizeof(p) == 8 // 64位系统
(二). 内存布局不同
数组 a 本身占据一块连续的内存,用来存放5个int:
a:
+------+------+------+------+------+
| a[0] | a[1] | a[2] | a[3] | a[4] |
+------+------+------+------+------+
0x100 0x104 0x108 0x10C 0x110
而指针 p 只是一个变量,它里面存的是地址:
p:
+----------+
| 0x100 | ---> 指向 a[0]
+----------+
0x200
图1:数组是“一排房子”,指针是“写着地址的纸条”。
也就是说,数组名 a 在大多数表达式中会被转换成“首元素地址”,但它本身并不存储这个地址,它只是代表那块内存。
(三). 可修改性不同
int a[5];
int *p = a;
p = p + 1; // 合法,p 是变量
a = a + 1; // 非法!a 不是可修改的左值
数组名 a 不能被赋值,因为它不是一个变量,而是代表数组这块内存的名字。指针 p 是一个变量,可以被修改。
三、什么时候“数组名退化”?
数组名在以下场景中会退化为指向首元素的指针:
1.作为函数参数传递
2.参与算术运算,如 a + 1
3.用于比较、赋值给指针变量
4.在 sizeof 之外的大多数表达式里
而在以下场景中,数组名不会退化:
1.sizeof(a) —— 得到整个数组大小
2.&a —— 得到“指向整个数组的指针”,类型是 int (*)[5]
3._Alignof(a) 等
看一个容易混淆的例子:
int a[5];
printf("%p\n", (void*)a); // a[0] 的地址
printf("%p\n", (void*)&a); // 整个数组的地址
printf("%p\n", (void*)(a+1)); // a[0] 地址 + 4
printf("%p\n", (void*)(&a+1)); // 整个数组地址 + 20
虽然 a 和 &a 的数值相同,但类型不同,+1 的步长也不同:
a -> int* , a+1 跳过 1 个 int (4字节)
&a -> int(*)[5] , &a+1 跳过整个数组 (20字节)
图2:a+1 与 &a+1 的区别
低地址 高地址
+------+------+------+------+------+
| a[0] | a[1] | a[2] | a[3] | a[4] |
+------+------+------+------+------+
^ ^ ^
a a+1 &a+1
&a
四、二维数组:更容易混淆的地方
int b[3][4];
b 的类型是 int[3][4],它的首元素是 b[0],类型是 int[4]。
所以:
b -> int (*)[4] // 指向“含4个int的数组”的指针
b[0] -> int[4] // 一个数组
b[0][0] -> int
当 b 作为函数参数时,会退化成 int (*)[4],而不是 int **。这也是很多人写二维数组传参时出错的原因:
void f(int (*p)[4], int rows) { ... } // 正确
void f(int **p, int rows) { ... } // 错误,类型不匹配
图3:二维数组的内存布局
b:
+---------+---------+---------+---------+
| b[0][0] | b[0][1] | b[0][2] | b[0][3] |
+---------+---------+---------+---------+
| b[1][0] | b[1][1] | b[1][2] | b[1][3] |
+---------+---------+---------+---------+
| b[2][0] | b[2][1] | b[2][2] | b[2][3] |
+---------+---------+---------+---------+
它是一整块连续内存,b[i] 是其中第 i 行的数组。
五、常见误区总结

六、一句话记住区别
数组是“一块连续内存”,指针是“存放地址的变量”。数组名在大多数场合会退化成指针,但它本身不是指针。
理解这一点,你就能避开C语言里最常见的一类陷阱:函数传参时 sizeof 失效、二维数组传参类型不匹配、a+1 和 &a+1 步长不同等等。
写代码时多问自己一句:
我现在操作的是“那块内存”,还是“存地址的变量”?
它退化了吗?退化成了什么类型?
把这两个问题想清楚,数组和指针就不再是容易混淆的概念了。

