12、二级指针与动态二维数组:用指针数组模拟二维数组、用数组指针模拟二维数组
二维数组,听起来挺简单吧?但一旦涉及到动态内存分配,事情就变得有意思了。很多初学者在这里栽跟头,说实话,我当年也迷糊了好一阵子。
今天我们就来彻底搞懂它。核心就两个东西:指针数组 和 数组指针。名字很像,但用法天差地别。
12.1 先搞清楚:二维数组在内存里到底长啥样?
静态二维数组 int arr[3][4],在内存中是连续存放的。就像一个长条,3行4列,一共12个int,挨个排好。
但动态分配就不一样了。我们可以用两种方式模拟出“二维”的效果。说白了,就是两种不同的内存布局。
核心区别:
- 指针数组:先分配一个指针数组(存行指针),再为每一行单独分配一维数组。内存不连续,但每行长度可以不同。
- 数组指针:一次性分配一块连续的大内存,然后用数组指针来管理。内存连续,但每行长度必须相同。
嗯,这里要注意:很多人把这两个概念搞混,面试时也经常被问到。我个人习惯用指针数组,因为灵活。但如果你追求性能,数组指针更优。
12.2 方法一:用指针数组模拟二维数组
这种方法,我称之为“先搭架子,再填内容”。
先分配一个指针数组,里面每个元素都是一个 int*。然后循环,为每个指针分配一行。
#include <stdio.h>
#include <stdlib.h>
int main() {
int rows = 3, cols = 4;
int **arr = (int**)malloc(rows * sizeof(int*)); // 指针数组
for (int i = 0; i < rows; i++) {
arr[i] = (int*)malloc(cols * sizeof(int)); // 每行
}
// 赋值
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
arr[i][j] = i * cols + j;
}
}
// 打印
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf("%d ", arr[i][j]);
}
printf("\n");
}
// 释放:先释放每行,再释放指针数组
for (int i = 0; i < rows; i++) {
free(arr[i]);
}
free(arr);
return 0;
}
我的经验: 我在项目中用这种方法处理过图像数据。每行宽度不一样,用指针数组就特别方便。但释放内存时一定要小心,顺序不能乱。我曾经因为先释放了指针数组,导致后面释放行时出现野指针,排查了半天。
避坑指南: 我曾经犯过一个低级错误——忘记检查 malloc 的返回值。如果内存分配失败,返回NULL,直接访问就会崩溃。所以,每次分配后都要加个判断:
if (arr == NULL) {
// 处理错误
}
12.3 方法二:用数组指针模拟二维数组
这种方法,我称之为“一次成型”。
直接分配一块连续的大内存,然后用数组指针来操作。数组指针的类型是 int (*p)[cols],意思是“指向一个包含cols个int的数组的指针”。
#include <stdio.h>
#include <stdlib.h>
int main() {
int rows = 3, cols = 4;
int (*arr)[cols] = (int(*)[cols])malloc(rows * cols * sizeof(int));
// 赋值
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
arr[i][j] = i * cols + j;
}
}
// 打印
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf("%d ", arr[i][j]);
}
printf("\n");
}
free(arr); // 一次释放
return 0;
}
关键点: 数组指针的语法有点绕。你想想看,int (*arr)[cols] 中,括号是必须的。如果写成 int *arr[cols],那就变成指针数组了。一个括号之差,含义完全不同。
为什么会这样?因为 [] 的优先级比 * 高。加上括号,(*arr) 表示 arr 是一个指针,然后 [cols] 表示这个指针指向一个数组。不加括号,*arr[cols] 表示 arr 是一个数组,数组元素是 int*。
12.4 两种方法的对比
| 特性 | 指针数组 | 数组指针 |
|---|---|---|
| 内存布局 | 不连续,每行独立分配 | 连续,一次性分配 |
| 每行长度 | 可以不同 | 必须相同 |
| 分配/释放 | 多次分配,多次释放 | 一次分配,一次释放 |
| 访问速度 | 稍慢(两次间接寻址) | 快(连续内存,缓存友好) |
| 适用场景 | 不规则二维结构(如锯齿数组) | 规则矩阵、图像处理 |
说白了,没有绝对的好坏。我在做矩阵运算时,一定用数组指针,因为连续内存对CPU缓存更友好。但做文本处理时,每行长度不一,指针数组就更合适。
12.5 一张图看懂两种方式
下面这张SVG图,展示了两种方式的内存布局差异。你看一眼就明白了。
12.6 总结一下
两种方式,各有千秋。指针数组灵活,但管理麻烦。数组指针高效,但限制多。
我个人建议:如果你不确定用哪个,先问自己一个问题——“每行长度是否固定?” 如果固定,用数组指针。如果不固定,用指针数组。
嗯,今天就到这里。记住,动态内存分配的核心是“谁分配,谁释放”。别漏了,也别重复释放。