第 4 章 · 指针与一维数组

数组名在多数表达式中退化成首元素指针,但有 sizeof& 两个例外。

3.1 数组名退化(array decay)

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*

3.2 两个例外: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]

3.3 数组传参:退化成指针

数组作为函数参数时必然退化——函数根本收不到「数组」,只收到一个指针,长度信息丢失:

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)。

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;
}

3.5 数组 vs 指针的对照表

表达式类型说明
a(值语境)int*首元素地址退化
&a[0]int*首元素地址显式取址
&aint (*)[5]数组起始地址指向整个数组
sizeof(a)size_t20整个数组大小

3.6 与汇编 / 其他语言对照

  • 汇编:数组退化正是「数组名 = 首地址」在 C 层的体现。访问 a[i] 变成 [base + i*size],数组本身的长度只存在于源码/编译器脑子里,运行时不带任何元数据——这是 C 高效的原因,也是越界不设防的原因。
  • Java:数组是对象,自带 length 字段,运行时有边界检查;不存在「退化」,数组和引用是两个明确不同的东西。
  • C++std::array<T,N> 用模板把长度带进类型,传参不丢失长度;裸 C 数组行为与 C 一致。
  • Pythonlist 自带长度与类型无关的元素,len() 是 O(1) 元数据查询。

① 数组名在值语境退化成首元素指针;② sizeof/& 是仅有两个例外;③ 进函数必退化为指针、长度丢失。