数组指针:从定义到实战

数组指针,这名字听着就有点绕。很多人刚学的时候,容易把它和指针数组搞混。我当年也踩过这个坑,面试时被问到“数组指针和指针数组的区别”,脑子一热就答反了……嗯,今天咱们就把这个硬骨头啃下来。

1. 数组指针的定义

说白了,数组指针就是一个指针,它指向的是一个数组。注意,不是指向数组里的某个元素,而是指向整个数组。

定义格式有点特别:

类型 (*指针名)[数组大小];

比如:

int (*p)[5];  // p是一个指针,指向一个包含5个int元素的数组

这里括号不能省。如果写成 int *p[5],那就变成指针数组了——一个数组,里面存了5个int*指针。我刚开始写代码时,就经常把括号漏掉,编译器报错还一脸懵。

核心记忆点:

  • int (*p)[5] → 数组指针(指针)
  • int *p[5] → 指针数组(数组)

怎么用呢?看个例子:

#include <stdio.h>

int main() {
    int arr[5] = {10, 20, 30, 40, 50};
    int (*p)[5] = &arr;  // 取整个数组的地址

    printf("arr[2] = %d\n", (*p)[2]);  // 输出30
    return 0;
}

注意,这里用的是 &arr,不是 arr。虽然 arr&arr 的值一样,但类型不同。arrint*,指向第一个元素;&arrint(*)[5],指向整个数组。这个区别在指针运算时特别明显——arr+1 跳过一个int,&arr+1 跳过整个数组(5个int)。

我的习惯: 在需要传递整个数组的地址时,我才会用数组指针。平时遍历元素,直接用数组名就行,没必要搞复杂。

2. 数组指针与二维数组

这才是数组指针真正发光的地方。二维数组在内存里是连续存储的,但它的类型本质上是“数组的数组”。比如:

int matrix[3][4];  // 3行4列

matrix 的类型是 int[3][4],退化后是 int(*)[4]——没错,就是一个数组指针,指向一个长度为4的int数组。所以,用数组指针来操作二维数组,非常自然。

看代码:

#include <stdio.h>

void print_matrix(int (*p)[4], int rows) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < 4; j++) {
            printf("%2d ", p[i][j]);  // p[i] 就是第i行,类型是int[4]
        }
        printf("\n");
    }
}

int main() {
    int matrix[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };

    print_matrix(matrix, 3);  // 数组名直接传,自动退化为 int(*)[4]
    return 0;
}

这里 p[i][j] 的写法,和直接用二维数组一模一样。为什么?因为 p[i] 等价于 *(p+i),拿到的是第i行的首地址,类型是 int*,然后再用 [j] 取第j个元素。

我曾经踩过的坑: 在函数参数里写 int p[][4]int (*p)[4] 是等价的,但很多人只记得第一种写法。有一次我接手一个遗留代码,里面全是 int **p 来传二维数组,结果一运行就段错误。因为 int**int(*)[4] 的内存布局完全不同,不能混用。

下面这张图,帮你理清数组指针和二维数组的关系:

数组指针与二维数组关系图 二维数组 int matrix[3][4] 第0行: matrix[0] → int[4] 第1行: matrix[1] → int[4] 第2行: matrix[2] → int[4] 数组指针 int (*p)[4] p 指向整个 int[4] 数组 *p 等价于 matrix[0] (首行) p[i] 等价于 matrix[i] 赋值 核心:二维数组名退化为数组指针,类型为 int(*)[4]

3. 数组指针作为函数参数

在实际项目中,我们经常需要把二维数组传给函数处理。用数组指针做参数,是最地道的方式。

函数声明有三种等价形式:

写法 说明
void func(int (*p)[4], int rows) 数组指针形式,最明确
void func(int p[][4], int rows) 数组形式,编译器自动解释为指针
void func(int p[3][4], int rows) 指定行数,但实际会被忽略

我个人偏爱第一种写法,因为它明确告诉读者:这里传进来的是一个指针,不是数组。第二种写法容易让人误以为在传值(数组会复制),其实C语言里数组传参都是传地址。

看一个实际点的例子——计算二维数组所有元素的和:

#include <stdio.h>

// 数组指针作为参数
int sum_matrix(int (*p)[4], int rows) {
    int total = 0;
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < 4; j++) {
            total += p[i][j];  // 或者 *(*(p+i)+j)
        }
    }
    return total;
}

int main() {
    int data[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };

    int result = sum_matrix(data, 3);
    printf("总和 = %d\n", result);  // 输出78
    return 0;
}

这里有个细节:p[i][j]*(*(p+i)+j) 完全等价。你想想看,p+i 跳过了 i 个长度为4的int数组,拿到第i行的地址;再解引用得到第i行的首元素地址;最后加 j 取元素。这套逻辑,其实就是二维数组下标运算的底层实现。

避坑指南:

  • 函数参数里,第二维的大小必须指定(比如这里的4),因为编译器需要知道每行有多少个元素才能正确计算偏移。
  • 如果第二维不确定,那就不能用数组指针了,得用 int ** 配合手动计算偏移,但那样代码会复杂很多。

我的经验: 在嵌入式开发中,经常要处理固定大小的图像数据(比如 320x240 的灰度图)。我会用 void process_image(uint8_t (*img)[240], int rows) 这样的接口,清晰又高效。曾经有个同事用 uint8_t ** 来传,结果每次都要自己算行偏移,还容易越界。后来我帮他改成数组指针,代码立马清爽了。

最后总结一下:数组指针就是“指向数组的指针”,它和二维数组是天作之合。用数组指针做函数参数,能让接口更清晰,也能避免很多低级错误。嗯,这部分内容就到这里,你可以在自己的代码里试试看。


公众号:蓝海资料掘金营,微信deep3321