17. 动态数组:使用 malloc 实现动态一维数组、动态二维数组的三种实现方式

动态数组,说白了就是「运行时才决定大小的数组」。

我刚开始学C语言时,总觉得静态数组 int arr[100] 挺好用的。直到有一次做嵌入式项目,需要根据传感器数量动态分配缓冲区——有的板子接8个传感器,有的接32个。你想想看,要是用静态数组,要么浪费内存,要么不够用。嗯,这时候就得靠 malloc 了。

17.1 动态一维数组

动态一维数组是最基础的操作。我个人的习惯是,先分配内存,再检查是否成功,最后用完了记得释放。

#include <stdio.h>
#include <stdlib.h>

int main() {
    int n;
    printf("请输入数组大小: ");
    scanf("%d", &n);

    // 分配内存
    int *arr = (int *)malloc(n * sizeof(int));
    if (arr == NULL) {
        printf("内存分配失败!\n");
        return -1;
    }

    // 使用数组
    for (int i = 0; i < n; i++) {
        arr[i] = i * 2;
    }

    // 打印结果
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    // 释放内存
    free(arr);
    return 0;
}
小提示:malloc 返回的是 void*,记得强制类型转换。虽然C语言允许隐式转换,但我建议显式写出来,代码更清晰。
警告:free 之后记得把指针置为 NULL。我曾经在一个项目中忘了这茬,导致野指针问题,排查了整整两天。

17.2 动态二维数组的三种实现方式

动态二维数组就有点意思了。说白了,C语言里没有真正的「动态二维数组」,我们得自己模拟。我总结了三种常用方法,每种都有各自的适用场景。

方式一:使用指针数组(行指针法)

这种方法最直观。先分配一个指针数组,每个指针再指向一行。

int **create_2d_array(int rows, int cols) {
    // 分配行指针数组
    int **arr = (int **)malloc(rows * sizeof(int *));
    if (arr == NULL) return NULL;

    // 为每一行分配内存
    for (int i = 0; i < rows; i++) {
        arr[i] = (int *)malloc(cols * sizeof(int));
        if (arr[i] == NULL) {
            // 分配失败,释放已分配的内存
            for (int j = 0; j < i; j++) {
                free(arr[j]);
            }
            free(arr);
            return NULL;
        }
    }
    return arr;
}

// 释放内存
void free_2d_array(int **arr, int rows) {
    for (int i = 0; i < rows; i++) {
        free(arr[i]);
    }
    free(arr);
}
优点:代码直观,每行长度可以不同(锯齿数组)。
缺点:内存不连续,CPU缓存不友好。我在做图像处理时吃过这个亏,访问速度比连续内存慢了30%。

方式二:使用一维数组模拟(连续内存法)

我个人最喜欢这种方式。说白了,就是把二维数组当成一维来用,通过下标计算访问。

int *create_2d_continuous(int rows, int cols) {
    // 一次性分配所有内存
    int *arr = (int *)malloc(rows * cols * sizeof(int));
    return arr;
}

// 访问元素:arr[row * cols + col]
#define GET(arr, row, col, cols) arr[(row) * (cols) + (col)]
#define SET(arr, row, col, cols, val) arr[(row) * (cols) + (col)] = (val)

// 使用示例
int main() {
    int rows = 3, cols = 4;
    int *arr = create_2d_continuous(rows, cols);

    // 赋值
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            SET(arr, i, j, cols, i * cols + j);
        }
    }

    // 打印
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            printf("%2d ", GET(arr, i, j, cols));
        }
        printf("\n");
    }

    free(arr);
    return 0;
}
技巧:用宏定义封装访问函数,代码可读性会好很多。我在嵌入式项目中经常这么干,既保持了性能,又让代码看起来像二维数组。

方式三:使用数组指针(固定列宽法)

这种方法适合列数固定的情况。嗯,说实话,用得不多,但某些场景下很优雅。

// 假设列数固定为 4
int (*arr)[4] = (int (*)[4])malloc(rows * sizeof(int[4]));

// 访问方式跟静态二维数组一样
arr[0][0] = 1;
arr[1][2] = 3;
注意:这种方式要求列数在编译时已知。如果你不确定列数,就别用这个。我曾经在一个跨平台项目中用了这个,结果换了个平台列数变了,代码全得重写。

17.3 三种方式对比

特性 指针数组法 连续内存法 数组指针法
内存连续性 不连续 连续 连续
访问速度 较慢 最快
锯齿数组支持 支持 不支持 不支持
代码复杂度 中等
释放复杂度 高(需逐行释放) 低(一次free) 低(一次free)
适用场景 锯齿数组、行数动态变化 高性能计算、图像处理 列数固定的场景

17.4 动态数组的核心流程图

下面这张图展示了动态二维数组三种实现方式的内存布局和访问逻辑。我画这张图时特意把指针和内存块的关系标清楚了,你一看就明白。

动态二维数组三种实现方式对比 方式一:指针数组法 arr[0] arr[1] arr[2] 行0数据 行1数据 行2数据 内存不连续 支持锯齿数组 释放需逐行free 方式二:连续内存法 arr[0][0] arr[0][1] arr[0][2] arr[0][3] arr[1][0] arr[1][1] arr[1][2] arr[1][3] arr[2][0] arr[2][1] arr[2][2] arr[2][3] 内存连续 CPU缓存友好 一次malloc一次free 方式三:数组指针法 (*arr)[4] 4列固定 4列固定 4列固定 列数编译时固定 访问语法最自然 灵活性最差 选择建议 需要锯齿数组 → 方式一 追求性能 → 方式二 列数固定且代码简洁 → 方式三

17.5 避坑指南

动态数组用起来方便,但坑也不少。我把自己踩过的坑总结一下:

  • 忘记释放内存:这是最常见的。我曾经在一个循环里反复 malloc 忘了 free,跑了三天程序直接崩了。
  • 释放不完全:方式一如果只 free(arr) 不 free 每行,会造成内存泄漏。我建议写一个专门的释放函数。
  • 越界访问:动态数组不检查边界,arr[rows][cols] 这种越界不会报错,但会破坏其他数据。调试起来特别痛苦。
  • realloc 使用不当:realloc 可能会移动内存,记得用返回值更新指针。我见过有人直接 arr = realloc(arr, new_size),结果 realloc 失败返回 NULL,原来的内存也丢了。
核心原则:谁分配,谁释放。malloc 和 free 要成对出现,最好在同一个函数或同一个模块中管理。

动态数组是C语言内存管理的必修课。我个人建议,新手先从方式二开始练手,等熟悉了再尝试方式一。方式三嘛,了解就好,实际项目中用得不多。

嗯,关于动态数组就聊这么多。记住一点:内存管理没有银弹,选哪种方式取决于你的具体场景。

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