4、指针与多维数组:二维数组的内存布局、行指针与列指针、指针数组与数组指针的区别、用指针操作二维数组
指针和二维数组,这俩东西凑一块,很多人就开始头疼了。说实话,我当年学C语言的时候,也是在这个地方卡了很久。二维数组到底在内存里怎么放的?行指针和列指针又是什么鬼?指针数组和数组指针到底谁是谁?
别急,咱们一个一个拆开看。搞明白了这些,你写底层驱动、图像处理、矩阵运算的时候,会顺手很多。
4.1 二维数组的内存布局
先问个问题:二维数组在内存里真的是“二维”的吗?
当然不是。内存是一维的,线性排列的。二维数组只是C语言给你的一种“语法糖”,让你觉得它像个表格。实际上,它在内存里就是一行接一行地排着。
比如你定义一个 int a[3][4],一共12个int,它们在内存里是连续存放的:
a[0][0] a[0][1] a[0][2] a[0][3] a[1][0] a[1][1] ... a[2][3]
这种存储方式叫“行优先存储”。C语言就是行优先的,Fortran是列优先。嗯,这个区别在跨语言调用的时候特别容易踩坑,我后面会提到。
核心理解:
a是整个二维数组的首地址a[0]是第0行的首地址&a[0][0]是第0行第0列的地址- 这三个值在数值上相等,但类型不同,意义也不同
类型不同,这个很关键。你想想看,a 的类型是 int (*)[4],也就是“指向包含4个int的数组的指针”。而 a[0] 的类型是 int *,就是普通的int指针。虽然它们指向同一个地址,但 a+1 跳过了16个字节(4个int),而 a[0]+1 只跳过了4个字节(1个int)。
我在项目中遇到过有人把 a 和 a[0] 混用,结果指针运算全乱套了。调试了半天才发现是类型不匹配的问题。
4.2 行指针与列指针
这两个概念,说白了就是“跳多大步子”的问题。
- 行指针:指向一整行。类型是
int (*)[N]。加1,跳过一整行。 - 列指针:指向一个元素。类型是
int *。加1,跳过一个元素。
看个例子就明白了:
int a[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
int (*row_ptr)[4] = a; // 行指针,指向第0行
int *col_ptr = &a[0][0]; // 列指针,指向第0行第0列
// row_ptr + 1 指向 a[1],即第1行
// col_ptr + 1 指向 a[0][1],即第0行第1列
我个人习惯在写矩阵运算的时候,用行指针来遍历行,用列指针来遍历列。这样代码意图更清晰,也不容易出错。
小技巧:
用 *(*(a + i) + j) 来访问 a[i][j]。这个写法虽然啰嗦,但能帮你理解指针的层级关系。a + i 是行指针,*(a + i) 解引用得到第i行的首地址(列指针),再加j再解引用,就拿到了元素。
4.3 指针数组与数组指针的区别
这个绝对是面试高频题,也是很多人容易搞混的地方。我当年面试的时候就被问过,答得磕磕巴巴的。
记住一句话:“指针数组”是一个数组,里面存的是指针;“数组指针”是一个指针,指向一个数组。
| 概念 | 定义方式 | 本质 | 典型用途 |
|---|---|---|---|
| 指针数组 | int *p[5] |
数组,元素是 int * |
存储多个字符串的首地址 |
| 数组指针 | int (*p)[5] |
指针,指向 int [5] |
指向二维数组的一行 |
怎么记?看运算符优先级。[] 的优先级比 * 高。所以 int *p[5] 先和 [] 结合,说明 p 是一个数组。而 int (*p)[5] 加了括号,* 先和 p 结合,说明 p 是一个指针。
我曾经在项目中用指针数组来管理多个传感器的数据缓冲区,每个缓冲区大小不同,用指针数组存它们的首地址,遍历起来非常方便。
// 指针数组:存储3个字符串
char *str_arr[3] = {"Hello", "World", "C Language"};
// 数组指针:指向一个包含4个int的数组
int arr[4] = {1, 2, 3, 4};
int (*p_arr)[4] = &arr;
4.4 用指针操作二维数组
掌握了上面的概念,用指针操作二维数组就水到渠成了。我一般用三种方式:
方式一:用数组下标(最直观)
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
printf("%d ", a[i][j]);
}
}
方式二:用行指针 + 列指针(灵活)
int (*row)[4] = a;
for (int i = 0; i < 3; i++) {
int *col = *row; // 或者 row[0]
for (int j = 0; j < 4; j++) {
printf("%d ", col[j]);
}
row++;
}
方式三:用一级指针直接遍历(效率高)
int *p = &a[0][0];
for (int i = 0; i < 12; i++) {
printf("%d ", p[i]);
}
第三种方式效率最高,因为少了一层间接寻址。但要注意,它把二维数组当成一维数组来用了,你得自己保证不越界。
避坑指南:
我曾经在嵌入式项目里用一级指针遍历二维数组,结果数组维度是 [3][4],我循环写了 i < 12,没问题。但后来需求变更,数组改成了 [4][4],我忘了改循环条件,还是 i < 12,结果漏掉了最后4个元素。嗯,从那以后我养成了用 sizeof 计算总元素数的习惯:sizeof(a) / sizeof(a[0][0])。
4.5 实际应用场景
说几个我在项目中用到的场景:
- 图像处理:一张灰度图就是一个二维数组。用行指针遍历每一行,用列指针处理每个像素。
- 矩阵运算:矩阵乘法、转置,用指针操作比下标快不少,尤其是大矩阵。
- 多通道数据采集:每个通道的数据是一维数组,多个通道组成二维数组。用指针数组管理每个通道的起始地址。
举个例子,图像处理中常用的“卷积操作”,本质上就是用指针在二维数组上滑动窗口:
// 3x3 卷积核,对图像做边缘检测
int kernel[3][3] = {
{-1, -1, -1},
{-1, 8, -1},
{-1, -1, -1}
};
// 用指针遍历图像(假设图像是 640x480)
int (*img)[640] = image; // 行指针
for (int y = 1; y < 479; y++) {
int *row = img[y];
for (int x = 1; x < 639; x++) {
// 用指针访问3x3邻域
int sum = 0;
for (int ky = -1; ky <= 1; ky++) {
int *krow = img[y + ky];
for (int kx = -1; kx <= 1; kx++) {
sum += krow[x + kx] * kernel[ky + 1][kx + 1];
}
}
// 处理结果...
}
}
你看,用行指针 img[y] 拿到当前行的首地址,然后通过 krow[x + kx] 访问邻域像素,代码既清晰又高效。
好了,二维数组和指针的关系,说白了就是“怎么在内存里找到你要的数据”。搞清楚了内存布局,理解了行指针和列指针的区别,再分清楚指针数组和数组指针,你就能自如地操作二维数组了。这些东西在底层开发中天天用,值得花时间彻底搞明白。
本章要点回顾:
- 二维数组在内存中是行优先连续存储的
- 行指针
int (*p)[N]指向一整行,列指针int *p指向一个元素 - 指针数组
int *p[N]是数组,数组指针int (*p)[N]是指针 - 用指针操作二维数组有三种方式:下标、行指针+列指针、一级指针遍历
- 实际项目中,指针操作比下标操作更灵活、效率更高