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图,展示了两种方式的内存布局差异。你看一眼就明白了。

两种动态二维数组的内存布局 指针数组 arr (int**) arr[0] arr[1] arr[2] 行0数据 (不连续) 行1数据 (不连续) 行2数据 (不连续) 数组指针 arr (int(*)[cols]) 连续内存块 行0 | 行1 | 行2 所有数据连续存放 一次malloc,一次free

12.6 总结一下

两种方式,各有千秋。指针数组灵活,但管理麻烦。数组指针高效,但限制多。

我个人建议:如果你不确定用哪个,先问自己一个问题——“每行长度是否固定?” 如果固定,用数组指针。如果不固定,用指针数组。

嗯,今天就到这里。记住,动态内存分配的核心是“谁分配,谁释放”。别漏了,也别重复释放。

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