数组名在多数表达式中退化成首元素指针,但有 sizeof、& 两个例外。
int a[5] = {1, 2, 3, 4, 5};
int *p = a; // 合法:a 在这里退化成 &a[0]
p[2]; // 等价 a[2]
数组名 a 在几乎所有「值」的语境下,都会隐式转成指向首元素的指针 &a[0]。所以 a 和 &a[0] 的值相同、类型都是 int*。
sizeof 与 &int a[5];
sizeof(a); // 20(5 × sizeof(int)),数组总大小——没有退化!
sizeof(&a[0]); // 8(64 位指针大小)——退化成指针了
&a; // 类型是 int (*)[5],「指向整个数组的指针」,不是 int*
&a[0]; // 类型是 int*,指向首元素
sizeof(a) 返回整个数组的大小,说明 a 在这里保留了数组身份。&a 的类型是 int (*)[5](指向含 5 个 int 的数组),而 &a[0] 是 int*。二者值相同(都指向数组起始地址),类型不同。int a[5];
int (*pa)[5] = &a; // 指向整个数组
(*pa)[2]; // 先解引用得到数组,再取下标 = a[2]
数组作为函数参数时必然退化——函数根本收不到「数组」,只收到一个指针,长度信息丢失:
void f(int a[5]); // 编译器等价于 int *a,方括号里的 5 被忽略
void f(int a[]); // 同样等价 int *a
void f(int *a); // 三种写法完全一致
函数内 sizeof(a) 是指针大小(8),不是数组大小——所以传数组必须额外传长度:
void sum(int *a, int n) {
int s = 0;
for (int i = 0; i < n; i++) s += a[i];
}
// 调用:sum(arr, 5);
sizeof(a)/sizeof(a[0])算元素个数只在数组定义的作用域内成立;进函数变指针后算错。要么传长度,要么传指向数组的指针(见 3.4)。
如果确实想让函数知道数组长度,可以传 int (*)[n]:
void fill(int (*a)[5]) { // 参数是「指向 5 元素数组的指针」
for (int i = 0; i < 5; i++) (*a)[i] = i;
}
int arr[5];
fill(&arr); // 必须传 &arr,类型才匹配
现代 C 还可用变长数组(VLA)参数让长度参与类型:
void fill(int n, int a[n]) { // n 在前,a 的类型是 int[n],仍会退化成 int*
for (int i = 0; i < n; i++) a[i] = i;
}
| 表达式 | 类型 | 值 | 说明 |
|---|---|---|---|
a(值语境) | int* | 首元素地址 | 退化 |
&a[0] | int* | 首元素地址 | 显式取址 |
&a | int (*)[5] | 数组起始地址 | 指向整个数组 |
sizeof(a) | size_t | 20 | 整个数组大小 |
a[i] 变成 [base + i*size],数组本身的长度只存在于源码/编译器脑子里,运行时不带任何元数据——这是 C 高效的原因,也是越界不设防的原因。length 字段,运行时有边界检查;不存在「退化」,数组和引用是两个明确不同的东西。std::array<T,N> 用模板把长度带进类型,传参不丢失长度;裸 C 数组行为与 C 一致。list 自带长度与类型无关的元素,len() 是 O(1) 元数据查询。① 数组名在值语境退化成首元素指针;②
sizeof/&是仅有两个例外;③ 进函数必退化为指针、长度丢失。