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)),其实 &arrarr 在这里效果一样,但类型不同。虽然没出 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 知识体系总览

下面这张图,把数组名退化的核心逻辑串起来了:

数组名 arr ❌ 不退化场景 sizeof(arr) / &arr ✅ 退化场景 arr[i] / arr+1 / 函数参数 退化为 &arr[0](首元素指针) 保持数组类型,不丢失长度 ⚠️ 常见陷阱 函数内 sizeof(arr) 误用 / 二维数组第一维退化 核心:记住两个特例 sizeof 和 & 不退化

3.8 总结几条实用规则

讲了这么多,最后给你几条我自己的经验总结:

  1. 函数传数组,必须带长度参数。别指望 sizeof 能帮你,它只会给你指针大小。
  2. 区分 arr&arr前者退化为首元素指针,后者是指向整个数组的指针,类型不同,步长不同。
  3. 字符数组初始化用 char arr[] = "..."别用 char *p = "..." 然后试图修改。
  4. 二维数组传参时,第二维大小必须明确。编译器需要知道每行有多少元素才能做指针运算。
  5. 实在搞不清的时候,用 typedef 定义数组类型。比如 typedef int arr5[5];,这样 arr5 *p = &arr; 就清晰多了。

💡 我的个人习惯:在写需要操作数组的函数时,我一般用 void func(int *arr, size_t len) 这种形式,明确告诉调用者:我接收的是指针,你需要告诉我长度。这样接口语义清晰,不容易出错。

数组名退化这个机制,说白了就是 C 语言在「高效」和「安全」之间做的取舍。它让数组操作更灵活、性能更好,但也把长度管理的责任交给了程序员。你只要记住那两个不退化的情况,其他时候把它当指针用,基本就不会踩坑了。


公众号:蓝海资料掘金营,微信deep3321