指针

彻底搞懂指针与数组的区别

指针和数组是 C 语言中最容易混淆的两个概念。很多初学者认为数组名就是指针,但实际上它们有本质区别。本文从内存模型、类型系统和编译器行为三个角度,彻底讲清指针与数组的关系。

一、数组名的退化规则

在大多数表达式中,数组名会退化为指向首元素的指针。但以下三种情况例外:

  • sizeof(arr) — 返回整个数组的字节数
  • &arr — 返回整个数组的地址,类型为 int (*)[N]
  • 字符串初始化 — char s[] = "abc"; 中的 s 是数组,不是指针
/* 数组名退化演示 */
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;       /* arr 退化为 &arr[0] */

printf("%zu\n", sizeof(arr));   /* 20 (5 * 4) */
printf("%zu\n", sizeof(p));    /* 8 或 4 (指针大小) */

二、&arr 与 arr 的本质区别

虽然 arr&arr 的数值相同,但类型完全不同,导致指针运算结果差异巨大:

int arr[5] = {10, 20, 30, 40, 50};

/* arr 退化为 int*,+1 跳过一个 int */
printf("%d\n", *(arr + 1));     /* 20 */

/* &arr 是 int (*)[5],+1 跳过整个数组 */
printf("%p\n", (void*)(arr + 1));    /* +4 字节 */
printf("%p\n", (void*)(&arr + 1));  /* +20 字节 */
关键结论:arr 的类型是 int*&arr 的类型是 int (*)[5]。前者 +1 移动 4 字节,后者 +1 移动 20 字节。

三、函数参数中的"数组"

函数参数中的数组声明会被编译器自动调整为指针。以下三种写法完全等价:

/* 三种写法完全等价 */
void foo(int arr[10]);
void foo(int arr[]);
void foo(int *arr);

/* 因此无法通过 sizeof 获取数组长度 */
void print_arr(int arr[10]) {
    printf("%zu\n", sizeof(arr));  /* 8 (指针大小!) */
}

四、二维数组与指针

二维数组名退化为指向数组的指针,而非二级指针:

int mat[3][4];
int (*p)[4] = mat;   /* 正确:p 是指向 int[4] 的指针 */
/* int **pp = mat; */ /* 错误:类型不匹配 */

/* 访问元素 */
mat[1][2] = 100;
p[1][2] = 100;
*(*(p + 1) + 2) = 100;  /* 三者等价 */

五、总结对照表

  • int arr[5] — 分配 20 字节连续内存,arr 不可赋值
  • int *p = arr — p 存储 arr 的首地址,p 可重新赋值
  • sizeof(arr) — 数组总大小(20)
  • sizeof(p) — 指针大小(4 或 8)
  • &arr — 整个数组的地址,类型 int (*)[5]
  • arr + 1 — 跳过 4 字节(一个 int)
  • &arr + 1 — 跳过 20 字节(整个数组)
记忆口诀:数组名不是指针,只是在表达式中"假装"成指针。sizeof 和 & 能看穿这个伪装。