第9章 数组指针:数组指针的定义、数组指针与二维数组的结合
数组指针,说白了就是一个指针,它指向的是整个数组,而不是数组里的某个元素。很多初学者容易把它和指针数组搞混——嗯,这两个名字确实容易让人头大。我当年刚学的时候也绕了好一阵子。
先记住一句话:数组指针的本质是指针,不是数组。它存的是数组的首地址,但它的类型信息里包含了数组的长度。
9.1 数组指针的定义
定义数组指针的语法,说实话有点反人类。我第一次看到的时候心里想:这谁设计的?
// 定义一个数组指针,指向含有5个int元素的数组
int (*p)[5];
注意括号不能少。如果写成 int *p[5],那就变成指针数组了——一个数组,里面存了5个int*指针。我见过不少同事在代码评审时被这个坑过。
为什么会这样?因为C语言的运算符优先级里,[] 的优先级高于 *。所以 int *p[5] 先和 p[5] 结合,说明 p 是个数组,然后每个元素是 int*。而 int (*p)[5] 加了括号,*p 先结合,说明 p 是个指针,指向一个含有5个int的数组。
核心记忆法:
int (*p)[n]—— p 是指针,指向长度为 n 的 int 数组int *p[n]—— p 是数组,每个元素是 int* 指针
我个人习惯这样理解:把 (*p) 看作一个整体,它是个指针,后面跟着 [5] 说明它指向的东西有5个元素。
9.2 数组指针的初始化与赋值
数组指针最常见的用法,就是让它指向一个二维数组的某一行,或者指向一个一维数组。
#include <stdio.h>
int main() {
int arr[5] = {1, 2, 3, 4, 5};
int (*p)[5]; // 定义数组指针
p = &arr; // 取整个数组的地址
// 通过数组指针访问元素
for (int i = 0; i < 5; i++) {
printf("%d ", (*p)[i]);
}
printf("\n");
return 0;
}
注意这里用的是 &arr,不是 arr。虽然 arr 和 &arr 的值是一样的,但类型不同。arr 的类型是 int*,指向第一个元素;&arr 的类型是 int (*)[5],指向整个数组。我刚开始学的时候觉得这区别没啥用,直到有一次做嵌入式开发,需要把整个数组作为参数传给DMA控制器——这时候类型匹配就很重要了。
小技巧: 用 sizeof 可以验证区别。sizeof(arr) 返回20(5个int),sizeof(&arr) 返回8(指针大小)。
9.3 数组指针与二维数组
这才是数组指针真正发光的地方。二维数组在内存里是连续存储的,数组指针可以让我们以行视角来操作它。
#include <stdio.h>
int main() {
int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
// 定义数组指针,指向含有4个int的数组
int (*row)[4];
// 让指针指向二维数组的首行
row = matrix; // 等价于 row = &matrix[0]
// 遍历所有元素
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
printf("%2d ", row[i][j]);
}
printf("\n");
}
return 0;
}
这里 row 是个数组指针,指向长度为4的int数组。row[i] 就是第 i 行,row[i][j] 就是第 i 行第 j 列的元素。你想想看,这其实和直接用 matrix[i][j] 是一样的效果。
为什么能这样?因为二维数组名 matrix 在表达式中会退化为指向其第一个元素的指针,而第一个元素是 matrix[0],它是一个长度为4的int数组。所以 matrix 的类型就是 int (*)[4]——正好和数组指针匹配。
我曾经踩过的坑: 把二维数组传给函数时,如果写成 void func(int **arr),编译器会报类型不匹配。正确的做法是用数组指针:void func(int (*arr)[4])。或者干脆用一维指针加行列参数:void func(int *arr, int rows, int cols)。
9.4 数组指针的运算
数组指针的加减运算,步长是整个数组的大小。这一点和普通指针很不一样。
#include <stdio.h>
int main() {
int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
int (*p)[4] = matrix;
printf("p = %p\n", p); // 第0行首地址
printf("p + 1 = %p\n", p + 1); // 第1行首地址
printf("p + 2 = %p\n", p + 2); // 第2行首地址
// 计算偏移量(字节)
printf("偏移量 = %ld 字节\n", (char*)(p + 1) - (char*)(p));
// 输出:偏移量 = 16 字节(4个int * 4字节)
return 0;
}
你看,p + 1 跳过了整整一行(16个字节),而不是一个int。这就是数组指针的步长特性。在嵌入式开发中,我经常用这个特性来遍历图像数据——每行有固定的像素数,用数组指针可以很方便地按行处理。
关键点总结:
- 数组指针
int (*p)[n]的步长是n * sizeof(int) p + i指向第 i 行的首地址*(p + i) + j指向第 i 行第 j 列的地址*(*(p + i) + j)等价于p[i][j]
9.5 数组指针作为函数参数
当我们需要把二维数组传给函数时,数组指针是最优雅的方式。它保留了数组的维度信息,让函数内部可以像操作二维数组一样操作数据。
#include <stdio.h>
// 参数是数组指针,必须指定第二维的大小
void print_matrix(int (*mat)[4], int rows) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < 4; j++) {
printf("%2d ", mat[i][j]);
}
printf("\n");
}
}
// 另一种写法:直接声明二维数组参数
// void print_matrix(int mat[][4], int rows) {
// 和上面的写法完全等价
// }
int main() {
int data[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
print_matrix(data, 3);
return 0;
}
这里有个细节:函数参数里的 int (*mat)[4] 和 int mat[][4] 是完全等价的。编译器会把后者也解释成指针。我个人更倾向于用数组指针的写法,因为它明确告诉读者:这是个指针,不是数组。
避坑指南: 函数参数中,第一维的大小可以省略,但第二维必须指定。因为编译器需要知道每行有多少个元素,才能正确计算地址偏移。我曾经在项目中看到有人把第二维也省略了,结果访问数据时全乱了。
9.6 知识体系图
下面这张图梳理了数组指针的核心概念和与二维数组的关系:
9.7 实际项目中的应用
我在做嵌入式图像处理时,经常需要处理帧缓冲区。帧缓冲区本质上就是一个二维数组,每行存储一行像素。用数组指针来处理,代码会清晰很多。
#include <stdio.h>
#include <stdint.h>
// 假设图像宽度为640像素,每个像素16位
#define IMG_WIDTH 640
#define IMG_HEIGHT 480
// 处理图像行:对每行做灰度化(简化示例)
void process_image(uint16_t (*frame)[IMG_WIDTH], int height) {
for (int y = 0; y < height; y++) {
for (int x = 0; x < IMG_WIDTH; x++) {
// 这里做像素处理
frame[y][x] = frame[y][x] & 0xFF; // 取低8位
}
}
}
int main() {
// 模拟帧缓冲区
static uint16_t buffer[IMG_HEIGHT][IMG_WIDTH];
// 初始化一些数据...
// 传入处理函数
process_image(buffer, IMG_HEIGHT);
return 0;
}
你看,用数组指针作为参数,函数内部可以直接用 frame[y][x] 访问像素,非常直观。如果换成一级指针,就得手动计算偏移量:frame[y * IMG_WIDTH + x],可读性就差多了。
本章核心要点:
- 数组指针
int (*p)[n]是指针,不是数组 - 它指向整个数组,步长是数组大小
- 与二维数组结合时,
p[i][j]等价于matrix[i][j] - 作为函数参数时,必须指定第二维的大小
- 在嵌入式开发中,常用于处理帧缓冲区、矩阵运算等场景
数组指针这东西,说白了就是个带类型信息的指针。它告诉编译器:我指向的东西有多大,你加减的时候心里有点数。理解了这一点,后面再学多维数组和指针的组合,就会顺畅很多。
公众号:蓝海资料掘金营,微信 deep3321