数组指针与指针数组:9000字干货全解析(图文版)

一、概念澄清:数组指针 vs 指针数组

1.1 数组指针:指向数组的指针

定义:数组指针是指向数组首元素地址的指针,其类型需明确指定数组长度(如int (*p)[5]表示指向含5个int的数组)。
核心特性

  • 内存连续性:数组指针解引用后得到的是整个数组的内存块。
  • 步长计算:指针算术运算以数组长度为单位(如p+1跳过5个int)。

示例代码

  1. int arr[5] = {1, 2, 3, 4, 5};
  2. int (*p)[5] = &arr; // p指向整个数组
  3. printf("%d\n", (*p)[2]); // 输出3

图示说明

  1. 内存布局:
  2. arr [1][2][3][4][5]
  3. p 指向arr的起始地址(包含5int

1.2 指针数组:存储指针的数组

定义:指针数组是数组元素为指针的数组(如int *p[3]表示含3个int*的数组)。
核心特性

  • 灵活性:每个元素可独立指向不同数据。
  • 应用场景:常用于字符串数组或动态数据结构。

示例代码

  1. int a = 10, b = 20;
  2. int *p[2] = {&a, &b}; // 指针数组
  3. printf("%d\n", *p[1]); // 输出20

图示说明

  1. 内存布局:
  2. p[0] 指向a的地址
  3. p[1] 指向b的地址

二、语法差异与常见陷阱

2.1 声明方式对比

类型 声明语法 优先级解析
数组指针 int (*p)[5] (*p)优先,表示p是指针
指针数组 int *p[5] *p优先,表示p是数组

陷阱案例

  1. int *p1[5]; // 指针数组:5个int*
  2. int (*p2)[5]; // 数组指针:指向含5个int的数组
  3. // 误用:int *p3[5] = &arr; // 错误!类型不匹配

2.2 内存分配与访问

动态分配示例

  1. // 数组指针动态分配
  2. int (*p)[3] = malloc(sizeof(int[3]));
  3. p[0][1] = 100; // 合法访问
  4. // 指针数组动态分配
  5. int **q = malloc(3 * sizeof(int*));
  6. q[1] = malloc(sizeof(int));
  7. *q[1] = 200; // 合法访问

关键区别

  • 数组指针需一次性分配连续内存(如malloc(sizeof(int[N])))。
  • 指针数组可分别分配每个元素的内存,适合非连续数据。

三、实战应用场景

3.1 二维数组的两种表示

方法1:数组指针

  1. void print_matrix(int (*mat)[3], int rows) {
  2. for (int i = 0; i < rows; i++) {
  3. for (int j = 0; j < 3; j++) {
  4. printf("%d ", mat[i][j]);
  5. }
  6. }
  7. }
  8. // 调用:int mat[2][3] = {{1,2,3},{4,5,6}}; print_matrix(mat, 2);

方法2:指针数组

  1. void print_matrix(int **mat, int rows, int cols) {
  2. for (int i = 0; i < rows; i++) {
  3. for (int j = 0; j < cols; j++) {
  4. printf("%d ", mat[i][j]);
  5. }
  6. }
  7. }
  8. // 调用:需先分配指针数组和每行内存

选择建议

  • 固定列数时优先用数组指针(类型安全)。
  • 动态列数或稀疏矩阵时用指针数组。

3.2 函数参数传递

传递数组指针

  1. void process_array(int (*arr)[10], int size) {
  2. // 确保arr指向的数组每行有10个元素
  3. }

传递指针数组

  1. void process_strings(char *strings[], int count) {
  2. // strings是字符串指针数组
  3. }

四、高级技巧与优化

4.1 类型别名简化代码

  1. typedef int (*IntArrayPtr)[5]; // 数组指针类型别名
  2. IntArrayPtr p = &arr; // 更清晰
  3. typedef int *IntPtr[3]; // 指针数组类型别名
  4. IntPtr q = {&a, &b, &c};

4.2 指针运算的边界检查

风险案例

  1. int arr[5];
  2. int (*p)[5] = &arr;
  3. p++; // 合法,跳过整个数组
  4. (*p)[5] = 10; // 危险!越界访问

建议

  • 使用sizeof计算边界:
    1. size_t arr_size = sizeof(arr) / sizeof(arr[0]);

五、常见问题解答

Q1:如何区分int *p[5]int (*p)[5]

记忆口诀

  • “指针数组”:先看*p,表示p是数组,元素为指针。
  • “数组指针”:先看(*p),表示p是指针,指向数组。

Q2:何时使用数组指针而非指针数组?

适用场景

  • 需要操作整个数组(如矩阵转置)。
  • 与C库函数交互(如memcpy接受void*,但数组指针可强制转换)。

总结表
| 特性 | 数组指针 | 指针数组 |
|——————————|———————————————|———————————————|
| 内存连续性 | 高 | 低(可分散) |
| 动态分配复杂度 | 低(单次分配) | 高(需逐元素分配) |
| 适用数据结构 | 固定大小多维数组 | 字符串集合、动态图结构 |

六、学习资源推荐

  1. 图解工具:使用Compiler Explorer实时查看指针内存布局。
  2. 练习题库
    • 编写函数交换两个数组指针的内容。
    • 实现动态二维数组的创建与释放。
  3. 进阶阅读:《C专家编程》第5章深入解析指针类型。

结语
数组指针与指针数组的差异源于C语言对指针和数组的严格类型检查。通过理解其内存模型和运算规则,开发者可避免90%的指针相关错误。建议结合本文图示与代码示例,在实际项目中逐步应用,最终达到“看声明知用途”的熟练度。”