指针与数组:数组名退化为指针

数组名到底是不是指针?这个问题,我当年刚学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]))

但这个宏只对真正的数组有效,对指针会给出错误结果。所以使用时一定要确认你传进去的是数组名,不是指针。

知识结构图

下面这张图总结了数组名退化的核心逻辑:

数组名退化机制 数组名 arr 使用场景 不退化的场景 sizeof(arr) → 整个数组大小 &arr → 指向整个数组的指针 退化的场景 arr → 指向 arr[0] 的指针 arr + i → 指向第 i 个元素 函数参数 int arr[] → int*

说白了,数组名退化是C语言为了兼容指针操作而设计的机制。它让数组可以用指针的方式访问,但同时也带来了不少陷阱。理解了这个机制,你就能在写代码时清楚地知道:什么时候数组名还是数组,什么时候它已经变成了指针。

记住一句话:数组名不是指针,但大多数时候它表现得像指针。区别就在 sizeof 和 & 这两个操作符上。