3、指针与数组:数组名退化为指针的陷阱
数组名到底是不是指针?这个问题,我面试过不少人,十有八九会答错。
很多人背过一句话:「数组名就是指针」。嗯,这话说对了一半。说它对,是因为大多数情况下数组名确实表现得像个指针;说它错,是因为在 sizeof 和取地址符 & 面前,它立马「现出原形」。
我个人习惯把这种现象叫做「退化」——数组名在大部分表达式中会退化为指向首元素的指针,但它的「本质」仍然是一个数组。搞不清这个,代码里就会埋下各种坑。
3.1 数组名 ≠ 指针:先看证据
咱们直接上代码。你猜猜下面这段会输出什么?
#include <stdio.h>
int main() {
int arr[5] = {1, 2, 3, 4, 5};
printf("sizeof(arr) = %zu\n", sizeof(arr));
printf("sizeof(&arr) = %zu\n", sizeof(&arr));
int *p = arr;
printf("sizeof(p) = %zu\n", sizeof(p));
return 0;
}
在 64 位系统上,输出是这样的:
sizeof(arr) = 20
sizeof(&arr) = 8
sizeof(p) = 8
看到了吗?sizeof(arr) 是 20(5 个 int × 4 字节),而 sizeof(p) 是 8(指针大小)。如果数组名就是指针,那 sizeof(arr) 也应该等于 8 才对。事实摆在眼前——数组名不是指针,它有自己的「身份」。
核心结论:
- 数组名在
sizeof操作中,代表整个数组 - 数组名在
&操作中,得到的是指向整个数组的指针 - 数组名在其他表达式中,退化为指向首元素的指针
3.2 退化发生的时机
那什么时候退化?什么时候不退化?我总结了一张表,你记牢了:
| 使用场景 | 是否退化 | 含义 |
|---|---|---|
sizeof(arr) |
❌ 不退化 | 返回整个数组的字节数 |
&arr |
❌ 不退化 | 得到指向整个数组的指针,类型为 int (*)[5] |
arr 作为函数参数 |
✅ 退化 | 退化为指针,丢失数组长度信息 |
arr[i] |
✅ 退化 | 等价于 *(arr + i) |
arr + 1 |
✅ 退化 | 指针运算,步长为元素大小 |
说白了,除了 sizeof 和 & 这两个「特例」,其他时候数组名都会乖乖退化成指针。这个规则,C 标准里写得明明白白。
3.3 最常见的陷阱:函数参数里的「假数组」
我在项目中遇到过好几次这样的 bug。你看这个函数:
void print_array(int arr[]) {
printf("数组大小: %zu\n", sizeof(arr));
// 你以为能拿到数组长度?想多了!
}
int main() {
int data[10] = {0};
printf("外部大小: %zu\n", sizeof(data)); // 40
print_array(data); // 8(64位系统)
return 0;
}
输出结果:
外部大小: 40
数组大小: 8
为什么会这样?因为函数参数里的 int arr[] 只是个「语法糖」,编译器会把它当作 int *arr 来处理。你写 int arr[] 也好,写 int *arr 也罢,在编译器眼里完全一样。
⚠️ 警告:永远不要在函数内部用 sizeof(arr) 来获取数组长度!传进来的 arr 已经退化成指针了,sizeof 拿到的只是指针大小。
正确的做法是:把数组长度作为另一个参数传进来。
void print_array(int arr[], size_t len) {
for (size_t i = 0; i < len; i++) {
printf("%d ", arr[i]);
}
}
3.4 &arr 和 arr 的区别
这个坑更隐蔽。你看:
int arr[5] = {0};
int *p1 = arr; // p1 指向 arr[0],类型 int*
int (*p2)[5] = &arr; // p2 指向整个数组,类型 int(*)[5]
printf("p1 = %p\n", p1);
printf("p1+1 = %p\n", p1 + 1); // 偏移 4 字节
printf("p2 = %p\n", p2);
printf("p2+1 = %p\n", p2 + 1); // 偏移 20 字节
p1 和 p2 的值虽然相同(都指向数组起始地址),但 p1+1 只跳过一个 int,p2+1 跳过了整个数组!
我记得有一次帮同事 review 代码,他写了个 memcpy(&arr, src, sizeof(arr)),其实 &arr 和 arr 在这里效果一样,但类型不同。虽然没出 bug,但这种写法容易让人困惑。我建议统一用 arr 就好,除非你真的需要指向整个数组的指针。
3.5 二维数组的退化:更复杂了
二维数组的退化规则,很多人一知半解。咱们看个例子:
int matrix[3][4] = {0};
// matrix 退化为 int (*)[4],指向第一行
// matrix[0] 退化为 int*,指向第一行第一个元素
void func(int arr[3][4]); // 等价于 void func(int (*arr)[4]);
// 注意:第一维的大小 3 被忽略了!
也就是说,二维数组作为函数参数时,只有第一维会退化,第二维的大小必须明确指定。你想想看,如果第二维也退化,编译器就不知道每行有多少个元素了,指针运算就没法做了。
💡 避坑指南:我曾经在写矩阵乘法时,把二维数组传给函数,结果在函数内部用 sizeof(arr)/sizeof(arr[0]) 来算行数——当然算错了。后来我老老实实把行数和列数都作为参数传进去,再也没出过问题。
3.6 字符数组 vs 字符指针
这个经典陷阱,几乎每个 C 程序员都踩过:
char str1[] = "hello";
char *str2 = "hello";
// str1 是数组,可以修改内容
str1[0] = 'H'; // ✅ 合法
// str2 指向字符串常量,修改会崩溃
str2[0] = 'H'; // ❌ 未定义行为,大概率崩溃
str1 是数组,在栈上分配了 6 字节空间(包括 '\0'),内容可修改。str2 是指针,指向只读数据段的字符串常量,内容不可修改。
我见过有人把 char *p = "abc"; 当数组用,然后试图修改它——程序直接崩了。嗯,这种 bug 排查起来还挺费劲的,因为不是每次都会崩,取决于编译器和运行环境。
3.7 知识体系总览
下面这张图,把数组名退化的核心逻辑串起来了:
3.8 总结几条实用规则
讲了这么多,最后给你几条我自己的经验总结:
- 函数传数组,必须带长度参数。别指望
sizeof能帮你,它只会给你指针大小。 - 区分
arr和&arr。前者退化为首元素指针,后者是指向整个数组的指针,类型不同,步长不同。 - 字符数组初始化用
char arr[] = "...",别用char *p = "..."然后试图修改。 - 二维数组传参时,第二维大小必须明确。编译器需要知道每行有多少元素才能做指针运算。
- 实在搞不清的时候,用
typedef定义数组类型。比如typedef int arr5[5];,这样arr5 *p = &arr;就清晰多了。
💡 我的个人习惯:在写需要操作数组的函数时,我一般用 void func(int *arr, size_t len) 这种形式,明确告诉调用者:我接收的是指针,你需要告诉我长度。这样接口语义清晰,不容易出错。
数组名退化这个机制,说白了就是 C 语言在「高效」和「安全」之间做的取舍。它让数组操作更灵活、性能更好,但也把长度管理的责任交给了程序员。你只要记住那两个不退化的情况,其他时候把它当指针用,基本就不会踩坑了。
公众号:蓝海资料掘金营,微信deep3321