C 数组与指针的关系
更新时间:2026-08-26。本文是
languages/c/主题入门层第 31 篇,也是通向指针专题的桥梁。数组和指针在 C 里"剪不断理还乱"——好消息是,理解一条规则就全通了:数组名是首元素地址,下标访问就是地址偏移。
本文要回答的问题
- 为什么说
arr[i]和*(arr + i)是同一回事? - 数组名到底是不是指针?
- 数组参数为什么"退化"成指针?
一、数组名是首元素地址
c
int arr[5] = {10, 20, 30, 40, 50};
printf("%p\n", arr); // 数组的起始地址
printf("%p\n", &arr[0]); // 首元素的地址 —— 和 arr 相同!arr 的值 = &arr[0]。数组名在绝大多数表达式里会"衰减"(decay)成指向首元素的指针。所以:
c
int *p = arr; // 合法:数组名衰减成指针,p 指向 arr[0]二、下标就是偏移:arr[i] ≡ *(arr + i)
C 标准里,arr[i] 的定义就是 *(arr + i)——"从首地址偏移 i 个元素,再解引用"。这一条规则衍生出几个奇观:
c
printf("%d\n", arr[2]); // 30:常规写法
printf("%d\n", *(arr + 2)); // 30:等价写法
printf("%d\n", 2[arr]); // 30:竟然也合法!(不推荐,纯粹炫技)2[arr] 合法是因为下标运算可交换:2[arr] == *(2 + arr) == *(arr + 2)。能用不代表该用——这只是帮你理解"下标本质是偏移"的思维实验。
三、指针算术:p + 1 不是"地址 + 1"
c
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;
printf("%p\n", p); // 假设 0x100
printf("%p\n", p + 1); // 0x104(int 4 字节,不是 0x101!)
printf("%d\n", *(p + 2)); // 30:第 3 个元素指针 +1 是"前进一个元素",地址增加的是 sizeof(类型) 字节。编译器知道指针类型,自动算偏移。这是指针运算的核心。
四、数组参数"退化"成指针
c
void f(int arr[], int n); // 这里的 int arr[] 只是 int *arr 的语法糖!数组传给函数时,整个数组不会被复制,传过去的是首元素地址。所以:
- 函数里
sizeof(arr)是 8(指针大小); - 函数里改
arr[i]会改到外部的数组(因为操作的是同一块内存); - 必须另传长度,函数自己不知道数组多大。
c
void sum_array(int arr[], int n) {
int total = 0;
for (int i = 0; i < n; i++) total += arr[i]; // 改的是外面的内存
}五、那数组名是指针吗?——不完全是
| 维度 | 数组名 arr | 指针 p |
|---|---|---|
| 类型 | int[5](数组类型) | int*(指针类型) |
| 可赋值 | 不行(arr = p 编译错误) | 可以 |
| 有存储 | 数组是一块内存 | 指针是 8 字节变量 |
| sizeof | 数组大小(20) | 8 |
| 表达式里 | 衰减成指针 | 就是指针 |
所以准确说法:数组名"表现得像"指向首元素的指针,但本身不是指针变量——它没有自己的存储,也不能被赋值。&arr(整个数组的地址)和 &arr[0](首元素地址)类型都不同,这是高手层指针与多维数组的内容。
六、常见坑对照
| 坑 | 现象 | 对策 |
|---|---|---|
| 以为数组名能赋值 | 编译错误 | 用指针变量 |
p + 1 当"地址+1字节" | 值不对 | 记住是元素偏移 |
| 函数内 sizeof 数组 | 拿到 8 | 另传长度 |
| 下标越界 | UB | 边界检查 |
&arr vs &arr[0] 搞混 | 类型警告 | 高手层细讲 |
七、与本站主线衔接
- 指针算术的完整世界,见指针运算;
- 数组名衰减的底层机制(左值右值转换),见专家层编译、链接与 ELF;
- 指针与数组在汇编里的等价(
lea算地址),见与汇编、性能剖析衔接。
一句话总结
数组名 = 首元素地址,arr[i] 就是 *(arr+i)(下标即偏移);指针 +1 前进一个元素(地址 + sizeof);数组传参退化成指针(sizeof 失效、改元素影响外面、必须带长度)——这一条等价关系,是 C 指针世界的入口。