指针与数组:数组名退化为指针
数组名到底是不是指针?这个问题,我当年刚学C语言时也纠结过。说实话,很多教材讲得模棱两可。今天咱们就把这个「退化」机制彻底说清楚。
数组名是什么?
先看一个最基础的现象:
int arr[5] = {1, 2, 3, 4, 5};
printf("%p\n", arr); // 输出数组首地址
printf("%p\n", &arr[0]); // 输出第一个元素的地址
这两行打印的结果一模一样。所以很多人说「数组名就是指针」。但真的是这样吗?
试试这个:
printf("%zu\n", sizeof(arr)); // 输出 20(5个int,每个4字节)
printf("%zu\n", sizeof(&arr[0])); // 输出 8(64位系统上指针大小)
看到了吗?sizeof(arr) 返回的是整个数组的大小,而不是指针的大小。这说明数组名和指针在本质上不是一回事。
核心结论:数组名本身是一个「数组类型」,不是指针。但在大多数表达式中,它会「退化」成指向首元素的指针。
什么时候退化?什么时候不退?
我习惯把退化规则记成一张表,工作中经常用到:
| 使用场景 | 是否退化 | 说明 |
|---|---|---|
sizeof(arr) |
❌ 不退 | 返回整个数组的字节数 |
&arr |
❌ 不退 | 取整个数组的地址,类型是 int (*)[5] |
arr 作为值使用 |
✅ 退化 | 变成 int*,指向 arr[0] |
arr + i |
✅ 退化 | 指针运算,指向第 i 个元素 |
arr[i] |
✅ 退化 | 等价于 *(arr + i) |
函数参数 int arr[] |
✅ 退化 | 编译器自动视为 int* arr |
嗯,这里要注意:&arr 和 arr 虽然打印出来的地址值一样,但类型完全不同。&arr 是「指向整个数组的指针」,对它加1会跳过整个数组。
int arr[5] = {0};
int *p = arr; // p 指向 arr[0]
int (*q)[5] = &arr; // q 指向整个数组
printf("%p\n", p + 1); // 地址增加 4 字节
printf("%p\n", q + 1); // 地址增加 20 字节
我在项目中遇到过有人把 &arr 和 arr 混用,结果指针运算时跳过了整个数组,数据全读错了。这种 bug 很难查,因为地址值看起来是对的。
函数参数中的退化
这是最容易被坑的地方。你写:
void print_array(int arr[]) {
printf("%zu\n", sizeof(arr)); // 输出 8,不是数组大小!
}
为什么?因为函数参数里的 int arr[] 只是个语法糖,编译器会把它当成 int* arr。说白了,数组在传参时已经「退化」成指针了。
避坑指南:我曾经在写一个网络协议解析模块时,在函数内部用 sizeof(arr)/sizeof(arr[0]) 计算数组长度,结果一直不对。查了半天才发现,传进来的数组已经退化成指针了。从那以后,我只要把数组传给函数,一定会额外传一个长度参数。
正确的做法是这样:
void print_array(int arr[], size_t len) {
for (size_t i = 0; i < len; i++) {
printf("%d ", arr[i]);
}
}
int main() {
int data[10] = {0};
print_array(data, sizeof(data)/sizeof(data[0]));
return 0;
}
二维数组的退化
二维数组的退化规则稍微复杂一点。你想想看:
int matrix[3][4] = {{0}};
int (*p)[4] = matrix; // matrix 退化成指向 int[4] 的指针
matrix 退化后不是 int**,而是 int (*)[4]。为什么?因为二维数组在内存中是连续存储的,每一行是一个长度为4的一维数组。退化时,它变成指向「第一个一维数组」的指针。
我见过有人这样写:
void func(int **p) { ... }
int matrix[3][4];
func(matrix); // 编译警告!类型不匹配
这是典型的错误。matrix 退化后是 int (*)[4],不是 int**。要接收它,参数类型必须匹配:
void func(int (*p)[4], int rows) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < 4; j++) {
printf("%d ", p[i][j]);
}
}
}
退化带来的常见陷阱
总结几个我踩过的坑:
- 陷阱1:在函数内部用
sizeof求数组长度。解决方案:传参时带上长度。 - 陷阱2:把
&arr和arr当成同一个东西。解决方案:记住&arr的类型是「指向数组的指针」。 - 陷阱3:二维数组传参时误用
int**。解决方案:明确列数,用int (*)[N]。 - 陷阱4:对退化后的指针做
++操作,以为还在操作原数组。解决方案:需要保留原数组地址时,先存一份。
个人习惯:我一般在代码里用宏来避免这类问题:
#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
但这个宏只对真正的数组有效,对指针会给出错误结果。所以使用时一定要确认你传进去的是数组名,不是指针。
知识结构图
下面这张图总结了数组名退化的核心逻辑:
说白了,数组名退化是C语言为了兼容指针操作而设计的机制。它让数组可以用指针的方式访问,但同时也带来了不少陷阱。理解了这个机制,你就能在写代码时清楚地知道:什么时候数组名还是数组,什么时候它已经变成了指针。
记住一句话:数组名不是指针,但大多数时候它表现得像指针。区别就在 sizeof 和 & 这两个操作符上。