第十一章 指针和数组的关系
本章要点
在第十章中,我们用 malloc 动态分配了一段内存,返回的是一个 int * 类型的指针,却可以像数组一样用 scores[i] 来访问它。这个现象自然而然引出一个问题:指针和数组之间,到底有什么关系? 再加上此前我们就注意到数组名在多数场合表现得像一个地址——这两条线索指向同一个方向:指针和数组在 C 语言中有着深层的关联。
在此之前,我们分别学习了数组(第三章)和指针(第七章)。数组是一片连续内存,用下标访问;指针存地址,用 * 间接访问。本章把这两个概念放在一起,彻底梳理清楚它们的关系。
本章把指针与数组的关系彻底梳理清楚。具体涵盖以下内容:
- 数组名和指针的关系——数组名是不是指针?退化机制、
[ ]的本质、sizeof与取地址的差异 - 数组和
malloc出的指针的关系——为什么堆内存也能当数组用?栈数组与堆内存的对比
理解了这些,你便能在两种表示之间自如切换,也为后续章节理解生命周期和动态内存打下基础。
一、数组名和指针的关系
1. 数组名——多数场景会”退化”为指针
当你写下 int arr[5]; 时,arr 这个名字代表的是整个数组——5 个 int 连在一起的一整块空间。然而,在绝大多数表达式中(sizeof 和 & 是少数例外),编译器会自动将 arr 转换为数组首元素的地址。这个行为通常被称为数组名到指针的”退化”(decay)。
int arr[5] = {10, 20, 30, 40, 50};
// arr 在表达式中退化为首元素地址,所以这两行打印的值相同
printf("arr = %p\n", (void *)arr);
printf("&arr[0] = %p\n", (void *)&arr[0]);
要注意的是,arr 退化后得到的是一个地址值,但 arr 本身并不是一个指针变量——它没有自己独立的存储空间来存这个地址。它只是数组的名字,在需要参与运算时被编译器”翻译”成了首元素地址。因此数组名不能像指针变量那样被重新赋值:
int arr[5];
int *p = arr; // 正确:把数组首地址赋给指针 p
arr = p; // 错误!数组名不能放在等号左边被赋值
一个有效的心理模型:你可以把
arr想象成一个固定不变的首元素地址——它的值永远是数组开头的位置,你不能修改它,但你可以读它、用它参与运算(arr + 1、arr[2]等等)。这个简化模型足够应付绝大多数初学场景。更精确的理解(数组名标识数组对象、在表达式中发生退化)会随着使用经验的积累自然建立起来,目前不必强求。
2. 数组名和指针的本质区别
上面我们看到,数组名退化后在表达式中表现得”像”一个指针。那么,数组名到底是不是指针?
答案很明确:不是。 arr 是数组的名字,p 是指针变量,两者在本质上完全不同:
- 数组是一块数据的容器——
int arr[5]在栈上分配了 5 个int连在一起的一整片空间,arr是这片空间的名字。 - 指针是一个寻址的工具——
int *p是一个独立的 8 字节变量,里面存着一张写着”你要找的数据在某某位置”的纸条。
正因为数组名不是指针变量,它们在具体行为上产生了实际的差异。下面逐一来看。
差异一:sizeof 的结果不同
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
printf("sizeof(arr) = %d\n", (int)sizeof(arr)); // 20(整个数组的字节数)
printf("sizeof(p) = %d\n", (int)sizeof(p)); // 8(指针本身的字节数,64位系统)
sizeof(arr):得到整个数组占用的内存大小——5 * sizeof(int),大多数平台上为 20 字节。sizeof是少数数组名不退化的例外之一,此时arr代表的是整片数组空间。sizeof(p):得到指针变量本身的大小,64 位系统上通常是 8 字节,与它指向什么数据毫无关系。
这个差异在将数组作为函数参数传递时尤为关键,后续学习函数与数组配合时自然会遇到。
差异二:取地址的行为不同
int arr[5];
printf("arr = %p\n", (void *)arr); // 首元素地址
printf("&arr = %p\n", (void *)&arr); // 整个数组的地址
// 这两个数值相同,但类型不同!
虽然 arr 和 &arr 打印出的地址值相同,但它们的类型截然不同:arr 在表达式中退化为 int *(指向首元素的指针),而 &arr 的类型是 int (*)[5](指向整个数组的指针)。这种类型差异会影响指针算术运算——对 arr 加 1 会前进 1 个 int 的距离,而对 &arr 加 1 则会前进整个数组的距离(即 5 * sizeof(int) 字节)。
对比指针变量:
int *p = arr;
printf("p = %p\n", (void *)p); // p 的值:arr 的首地址
printf("&p = %p\n", (void *)&p); // 指针变量 p 自己的地址(和 p 的值完全不同!)
p 是一个独立的指针变量,它有自己的存储空间。p 的值是数组的首地址,而 &p 是 p 这个变量自身在内存中的地址——两者毫不相干。
差异三:数组名不可修改
int arr[5];
int *p = arr;
p++; // 正确:p 是变量,可以改变指向
arr++; // 错误!数组名不能自增
数组名代表整块数组空间本身,它不是一个单独存放地址的变量——这才是它不能放在赋值号左边、也不能执行 ++ 的根本原因。指针变量则不同,它是一个实实在在的变量,里面存了一个地址,可以随时改写成另一个地址,指向不同的数据。
二、数组和 malloc 出的指针的关系
第一部分回答了”数组名是不是指针”——不是。现在来看另一个同样重要的问题:malloc 返回的明明是一个指针,为什么能把它当数组用?
回忆一下第十章中这个程序片段——我们让用户输入学生人数 n,然后 malloc 了一块内存来存放成绩,紧接着就用 scores[i] 访问了它:
int n;
printf("请输入学生人数:");
scanf("%d", &n);
int *scores = malloc((size_t)n * sizeof(int));
if (scores == NULL) return 1;
// 像使用数组一样使用 scores!
for (int i = 0; i < n; i++)
{
printf("请输入第 %d 个学生的成绩:", i + 1);
scanf("%d", &scores[i]); // ← scores 明明是指针,却用了 [i]
}
for (int i = 0; i < n; i++)
{
printf("%d ", scores[i]); // ← 这里也是
}
scores 是一个 int * 类型的指针变量,为什么能对它写 scores[i]?要回答这个问题,得先从 [ ] 这个符号本身说起。
1. [ ] 的真相——为什么指针也能用下标访问?
这背后的机制其实在第七章已经埋下了:p[i] 和 *(p + i) 是完全等价的。 指针可以加一个整数做移动——p + i 就是从 p 当前位置向高地址方向移动 i 个元素。编译器看到 p[i],内部就是把它翻译成 *(p + i) 来处理的。
换句话说,上面第十章代码中的 scores[i],在编译器眼里就是 *(scores + i)。而 scores + i 正好指向第 i 个 int 的位置——这一切和 scores 是不是”数组”毫无关系,它只要求 scores 是一个指针、且它指向的内存是连续排列的。
[ ] 只是指针算术运算的一种简写形式,从来就不是数组的”专利”。
2. 指针算术运算——为什么 arr[i] 和 *(arr + i) 等价
理解了 [ ] 的本质,再来看数组名。数组名在表达式中会退化为首元素地址,那么”第 i 个元素”的地址显然可以通过首地址加上偏移量算出:
首地址 + 偏移量 → 第 i 个元素的地址
C 语言的指针加法会自动根据指向的类型进行缩放。例如 int *p 加 1,地址值实际增加的是 sizeof(int) 个字节(通常是 4),正好跳过一个 int。
int arr[5] = {10, 20, 30, 40, 50};
// 下面两行完全等价
printf("%d\n", arr[2]); // 输出 30
printf("%d\n", *(arr + 2)); // 输出 30
arr[i] 的本质就是 *(arr + i)——这不是巧合,而是 C 语言标准规定的语义。编译器看到 arr[i],先让 arr 退化为首地址,再按 *(首地址 + i) 的方式去访问内存。同样的逻辑也适用于任何指向连续内存的指针。
回到第十章的 scores:scores[i] 被翻译成 *(scores + i),而 scores + i 正是通过 malloc 分配的那块堆内存中第 i 个 int 的位置。这也正是第十章代码中,注释里写下 scores + i 和 *(scores + i) 的原因——它们和 &scores[i]、scores[i] 分别是完全等价的写法。
3. 用指针遍历数组
[i] = *(p + i) 意味着遍历同一段连续内存可以有多种写法。直接用第十章的 scores 为例,三种方式的对比一目了然:
// 沿用第十章的变量:int *scores, int n
// scores 指向堆上一段连续的 int 空间
// 方法1:下标法(最常用,第十章原版写法)
for (int i = 0; i < n; i++)
{
printf("%d ", scores[i]);
}
// 方法2:指针偏移法(等价写法)
for (int i = 0; i < n; i++)
{
printf("%d ", *(scores + i)); // scores[i] == *(scores + i)
}
// 方法3:移动指针法(p 自己往前走)
int *p = scores; // p 指向首元素
for (int i = 0; i < n; i++)
{
printf("%d ", *p);
p++; // p 移动到下一个元素
}
三种写法输出完全一致,各自适用的场景也不同:
- 下标法:最直观——明确表达了”访问第几个元素”的语义,推荐日常使用
- 指针偏移法:更贴近底层真相——
scores + i算出地址,*取出值,有助于理解编译器的视角 - 移动指针法:在字符串处理、链表遍历等算法中非常常见,是 C 语言惯用的遍历模式
4. malloc 指针为什么能当数组用?
现在可以完整回答了。第三章讲过,数组的本质是一段连续排列的内存空间——元素一个接一个,中间没有缝隙。而第十章中 malloc 做的事情,恰恰也是在内存中划出一块连续的区域并返回其起始地址。
对比一下第三章的栈数组和第十章的堆内存:
栈数组:int arr[5]; → 栈上分配 5 个连续的 int
堆内存:int *scores = malloc(n * sizeof(int)); → 堆上分配 n 个连续的 int
虽然一个在栈上、一个在堆上,但它们在内存中的排布方式是完全一样的——都有一个首地址,后面紧挨着一排同类型的元素。而 [i] 只是”从首地址出发、向高地址移动 i 步、取出那个位置的值”的简写形式——内存在栈上还是堆上,丝毫不影响这个机制的运作。
5. 栈数组和堆内存的对比
相似性说完了——那它们在使用上到底有什么区别?回到第十章的 scores 和第三章的 int arr[5],对比一下:
栈数组 int arr[5] | malloc 指针(第十章 scores) | |
|---|---|---|
| 大小决定时机 | 编译时写死,5 必须写常量 | 运行时按需决定,n 来自 scanf |
| 生命周期 | 自动管理,离开 { } 就释放 | 手动管理,不 free 就永远占着 |
sizeof 结果 | 整个数组的字节数(20) | 指针变量本身的大小(8 字节) |
| 能否改变大小 | 不能 | 可以通过 realloc 调整 |
一句话总结:栈数组和 malloc 指针在”用下标访问元素”这件事上用法完全一致,但在”从哪来、活多久、占多大”这些维度上是两套完全不同的规则。 这也正是第三章(固定数组)和第十章(动态内存)各自独立成章的缘由——它们解决的是同一个问题(管理一组同类型数据),但适用的场景和掌控的复杂度完全不同。
三、动手练习
#include <stdio.h>
int main(void)
{
// 练习1:用三种不同方式输出数组
int arr[6] = {1, 3, 5, 7, 9, 11};
printf("练习1:三种遍历方式\n");
// 方式1:下标法
for (int i = 0; i < 6; i++) printf("%d ", arr[i]);
printf("\n");
// 方式2:指针偏移法
for (int i = 0; i < 6; i++) printf("%d ", *(arr + i));
printf("\n");
// 方式3:移动指针法
int *p = arr;
for (int i = 0; i < 6; i++) printf("%d ", *p++);
printf("\n\n");
// 练习2:验证 sizeof 的区别
printf("练习2:sizeof 区别\n");
printf("sizeof(arr) = %d(整个数组)\n", (int)sizeof(arr));
printf("sizeof(p) = %d(指针本身)\n\n", (int)sizeof(p));
// 练习3:指针数组和数组指针的简单演示
int a = 10, b = 20, c = 30;
// 指针数组:数组里存指针
int *ptr_arr[3] = {&a, &b, &c};
printf("练习3:指针数组\n");
for (int i = 0; i < 3; i++)
{
printf("ptr_arr[%d] 指向的值:%d\n", i, *ptr_arr[i]);
}
// 数组指针:指向整个数组
int matrix[3] = {100, 200, 300};
int (*arr_ptr)[3] = &matrix;
printf("\n数组指针:通过 (*arr_ptr)[i] 访问\n");
for (int i = 0; i < 3; i++)
{
printf("(*arr_ptr)[%d] = %d\n", i, (*arr_ptr)[i]);
}
return 0;
}