核心思想一句话总结
- 指针数组:它是一个数组,数组里的每个元素都是一个指针。
int *arr[5];-> 一个包含5个int*类型元素的数组。
- 指向数组的指针:它是一个指针,这个指针指向了一个数组。
int (*parr)[5];-> 一个指向int[5]类型数组的指针。
记住这个核心区别,下面我们展开细说。

(图片来源网络,侵删)
指针数组
定义
指针数组,顾名思义,是一个数组,它的每个元素都是一个指针,最常见的是字符串数组,即一个字符指针数组。
语法
类型 *数组名[元素个数];
char *str_array[4];
[]的优先级高于 ,str_array先和[4]结合,表示str_array是一个包含4个元素的数组。- 然后剩下的
char *表示这个数组的每个元素都是一个指向char类型的指针(也就是字符串字面量的首地址)。
内存模型
想象一下,它就像一个架子,架子上放了4个标签(指针),每个标签都指向一个字符串(一个连续的内存空间)。

(图片来源网络,侵删)
代码示例
#include <stdio.h>
int main() {
// 定义一个指针数组,它包含3个 char* 指针
char *fruits[3] = {
"Apple", // "Apple" 是一个字符串字面量,其首地址被赋给 fruits[0]
"Banana", // "Banana" 的首地址被赋给 fruits[1]
"Cherry" // "Cherry" 的首地址被赋给 fruits[2]
};
// 遍历指针数组
// 注意:这里我们直接使用 %s,因为 fruits[i] 是一个 char* 指针,printf会从它指向的地址开始打印字符,直到遇到'\0'
for (int i = 0; i < 3; i++) {
printf("Fruit %d: %s\n", i, fruits[i]);
// 也可以这样理解:
// printf("Fruit %d: %s\n", i, *(fruits + i));
}
// 访问第一个字符串的第二个字符
printf("The second letter of the first fruit is: %c\n", fruits[0][1]); // 输出 'p'
return 0;
}
主要用途
- 存储多个字符串:这是指针数组最经典的应用场景,比如存储文件名、命令行参数等。
- 管理多个不同类型的变量:虽然不如字符串常见,但也可以创建一个
void*指针数组来管理不同类型的数据。
指向数组的指针
定义
指向数组的指针,它本身是一个指针,但它不指向单个变量,而是指向一个完整的数组,在处理多维数组时,这个概念非常有用。
语法
类型 (*指针名)[元素个数];
int (*parr)[5];
- 是必须的,它强制 先和
parr结合,表示parr是一个指针。 - 然后剩下的
int [5]表示这个指针指向一个包含5个int元素的数组。
内存模型
它就像一个“大指针”,这个指针的“步长”不是一个 int 的大小,而是一整个数组的大小(比如5个 int 的大小)。

(图片来源网络,侵删)
代码示例
#include <stdio.h>
int main() {
// 定义一个包含3个元素的数组,每个元素本身又是一个包含5个int的数组
int matrix[3][5] = {
{1, 2, 3, 4, 5},
{6, 7, 8, 9, 10},
{11, 12, 13, 14, 15}
};
// 定义一个指向包含5个int的数组的指针
int (*p)[5]; // p 是一个指针,指向一个 int[5] 的数组
// 让 p 指向 matrix 的第一行
// matrix 在表达式中会“退化”为其首元素地址,即第一行的地址,类型是 int(*)[5]
p = matrix;
// 遍历矩阵
// 外层循环遍历行数 (3行)
for (int i = 0; i < 3; i++) {
// 内层循环遍历列数 (5列)
for (int j = 0; j < 5; j++) {
// 访问方式 1:使用下标法 (p[i][j])
printf("%d ", p[i][j]);
// 访问方式 2:使用指针算术 (p + i) 指向第 i 行
// *(p + i) 是对第 i 行数组进行解引用,得到第 i 行的首元素地址
// ( *(p + i) ) + j 指向第 i 行第 j 列的元素
// printf("%d ", *(*(p + i) + j));
}
printf("\n");
}
// 指针算术的步长演示
printf("\nDemonstrating pointer arithmetic:\n");
printf("Address of p: %p\n", (void*)p);
printf("Address of p + 1: %p\n", (void*)(p + 1));
printf("Difference: %ld bytes\n", (char*)(p + 1) - (char*)p);
// 输出会是 20 (5 * sizeof(int)),说明 p+1 跳过了整整一行(5个int)
return 0;
}
主要用途
- 作为函数参数:当需要将一个多维数组作为参数传递给函数时,使用指向数组的指针作为形参是最高效、最标准的方式,它能让函数正确地计算内存偏移。
- 处理多维数组:在遍历和操作多维数组时,它提供了一种比下标法更底层、更灵活的访问方式。
对比与总结
| 特性 | 指针数组 (int *p[5]) |
指向数组的指针 (int (*p)[5]) |
|---|---|---|
| 本质 | 数组 | 指针 |
| 数组元素 | 每个元素都是一个指针 (int*) |
指针本身指向一个完整的数组 (int[5]) |
| 内存模型 | 一组分散的指针,指向各自的数据 | 一个单一的指针,指向一块连续的内存(一个数组) |
| 语法 | 类型 *数组名[大小]; |
类型 (*指针名)[大小]; |
| 步长 | p + 1 的步长是 sizeof(int*) (一个指针的大小) |
p + 1 的步长是 sizeof(int[5]) (整个数组的大小) |
| 主要用途 | 存储多个字符串、管理多个不同类型变量 | 作为函数参数传递多维数组、操作多维数组 |
| 类比 | 一个装满遥控器的架子 | 一个可以控制整个电视面板的遥控器 |
常见误区与陷阱
混淆 int* a[5] 和 int (*a)[5]
这是最经典的混淆点。运算符优先级是关键:
int *a[5]->int *(a[5])->a是一个数组,其元素是int*。int (*a)[5]->a是一个指针,指向int[5]。
用 int *p[N] 去处理多维数组
// 错误用法示例
int matrix[3][5] = { ... };
int *p[3]; // 定义一个指针数组
// 尝试让它指向 matrix 的每一行
for (int i = 0; i < 3; i++) {
p[i] = matrix[i]; // matrix[i] 是第 i 行的首元素地址,类型是 int*
}
// 这样看起来可以工作...
printf("%d\n", p[0][1]); // 输出 2
// ...p 和 matrix 的关系是松散的,p 中的指针只是“借用”了 matrix 的空间。
// matrix 生命周期结束,p 中的指针就会变成“野指针”。
// 而指向数组的指针 `int (*p)[5] = matrix;` 则与 matrix 有更强的逻辑关联。
虽然技术上可以这样做,但它破坏了多维数组的连续性,容易出错,使用 int (*p)[N] 是更健壮、更符合逻辑的方式。
- 指针数组:一个装着指针的盒子,适合用来管理一组独立的字符串或对象。
- 指向数组的指针:一个能指向整个盒子的遥控器,适合用来操作一个完整的、连续的数据块,尤其是多维数组。
理解这两者的区别是掌握 C 语言指针进阶用法的关键一步,多看代码、多动手练习,很快就能熟练运用它们了。
