「数组名在函数里 sizeof 怎么变成 8 了?」是我大一收到的求助里频率最高的问题,没有之一。答案藏在本章主角「数组退化(array decay)」里:C 的数组名在大多数场合会悄悄变成指针,数组本身的长度信息只活在编译器脑子里,运行时根本不存在。这一章讲清楚退化的规则、仅有的两个例外、以及工程上的防丢长度三板斧。原文小节编号错成 3.x,一并修正。
int a[5] = {1, 2, 3, 4, 5};
int *p = a; // a 在这里退化成 &a[0]
p[2]; // 就是 a[2]
在几乎所有「值语境」下,数组名隐式转换成指向首元素的指针。这就是第 3 章能写 a + 1、*(a + i) 的原因:那些运算根本不是作用在「数组」上,而是作用在退化出来的指针上。数组名和首元素指针,值相同,类型都是 int*。
退化规则有三处例外中的两处在这里(第三处字符串字面量初始化数组,第 6 章讲):
int a[5];
sizeof(a); // 20:整个数组的大小,数组身份还在
sizeof(&a[0]); // 8:退化成指针了
&a; // 类型 int (*)[5],指向整个数组,不是 int*!
&a[0]; // 类型 int*,指向首元素
&a 和 &a[0] 数值相同(都指着同一块地的门口),类型不同(&a 是「指向含 5 个 int 的数组的指针」)。类型不同意味着步长不同:&a + 1 一步跨过整个数组(20 字节),&a[0] + 1 只前进 4 字节。这个「指向整个数组的指针」看着别扭,却是第 5 章多维数组的敲门砖,这里先记住存在:
int (*pa)[5] = &a;
(*pa)[2]; // 先解引用得到整个数组,再下标,= a[2]
数组一旦作为函数参数,必然退化,写法上三种声明完全等价:
void f(int a[5]); // 编译器眼中的 int *a,方括号里的 5 被无视
void f(int a[]); // 同上
void f(int *a); // 三兄弟本是一个人
于是那个经典 bug 有了完整解释:
void print_size(int a[]) {
printf("%zu\n", sizeof(a) / sizeof(a[0])); // 8/4 = 2,不是 5!
}
函数收到的只是指针,sizeof(a) 是指针大小,长度信息在门口就丢了。这也是为什么 C 的数组参数约定俗成带一个 n:
int sum(const int *a, int n) { // 长度手动同行
int s = 0;
for (int i = 0; i < n; i++) s += a[i];
return s;
}
sizeof(a)/sizeof(a[0]) 这个「数组元素个数」惯用法,只在数组定义所在的作用域成立,进了函数就是错的。想编译期报错,可以加一行卫士(利用「指针不能对指针取模」的规则):static_assert(sizeof(a) != sizeof(int*), "数组已退化"),这在 review 里能救很多新人。
路线一:指向数组的指针。利用 4.2 的 int (*)[5],把长度写进类型:
void fill(int (*a)[5]) { // 长度 5 是类型的一部分,传错编译不过
for (int i = 0; i < 5; i++) (*a)[i] = i;
}
int arr[5];
fill(&arr); // 必须传 &arr
代价是死长度(长度变化就得改类型签名),适合「固定尺寸缓冲区」场景(比如像素行、页帧)。
路线二:VLA 参数。C99 起允许参数声明引用前面的参数:
void fill(int n, int a[n]) { // n 在前,a 顺势声明为 int[n]
for (int i = 0; i < n; i++) a[i] = i;
}
它仍然退化(运行时还是指针),但签名表达了意图,读者一眼看懂 a 长度是 n。工程里更常见的第三选择是直接传结构体(数组 + 长度打包),或者用 C++ 的 std::span 这类「指针 + 长度」的官方封装,那是后话。
int a[5] 参数里的 5 有编译器帮助。编译器连警告都不给(除非开 -Wsizeof-array-argument 这类冷门警告),长度永远自己管。sizeof(a)、sizeof(&a[0])、(void*)&a 与 (void*)&a[0],以及 (char*)&a + 1 - (char*)&a 与 (char*)&a[0] + 1 - (char*)&a 两个差值,亲眼看「同地址不同类型不同步长」。void f(int a[10]) 内 sizeof(a) 是多少?想让它编译期等于 40,有几种办法(至少两条),各自代价是什么?reverse(int *a, int n) 原地翻转数组,分别用下标版和纯指针版(只用 p、q 两个指针)各写一遍,验证等价。