18、动态数组的实现:用malloc实现动态数组、动态数组的扩容策略、动态二维数组

说实话,C语言的数组有个硬伤——长度必须在编译时就定死。你想想看,很多时候我们写程序,根本不知道用户会输入多少数据。比如一个日志系统,今天可能只记录10条,明天突然来了1000条。静态数组怎么搞?要么浪费空间,要么直接崩掉。

这时候就得靠动态数组了。说白了,就是用malloc在堆上申请内存,运行时想多大就多大。我当年刚入行时,第一次用动态数组解决了一个内存溢出的bug,那种感觉……嗯,就像手里突然多了一把万能钥匙。

18.1 用malloc实现动态数组

动态数组的核心就三步:申请、使用、释放。别小看第三步,我见过太多人只记得malloc,不记得free。结果程序跑几天,内存就爆了。

先看一个最简单的例子:

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

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

    // 动态申请 n 个 int 的空间
    int *arr = (int *)malloc(n * sizeof(int));
    if (arr == NULL) {
        printf("内存申请失败!\n");
        return 1;
    }

    // 像普通数组一样使用
    for (int i = 0; i < n; i++) {
        arr[i] = i * 10;
    }

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

    // 千万别忘了释放
    free(arr);
    return 0;
}

关键点:

  • malloc返回的是void*,记得强制类型转换。虽然C语言允许隐式转换,但我个人习惯显式写出来,代码更清晰。
  • 一定要检查返回值是否为NULL。内存申请失败不是开玩笑的,我曾经在嵌入式设备上遇到过堆空间不足,程序直接跑飞。
  • free之后,建议把指针置为NULL。防止出现野指针,这是很多隐蔽bug的根源。

18.2 动态数组的扩容策略

动态数组的好处是能长大。但怎么长大?直接realloc?没那么简单。扩容策略选不好,性能会惨不忍睹。

我给大家讲三种常见的扩容策略,每种我都踩过坑:

策略 描述 时间复杂度 适用场景
固定增量 每次扩容增加固定大小(如每次加10个元素) O(n) 均摊 数据量小且可预测
倍数扩容 每次扩容为当前容量的2倍 O(1) 均摊 大多数通用场景
指数衰减 初始扩容倍数大,后续逐渐减小 O(1) 均摊 内存敏感型系统

我个人最推荐倍数扩容,尤其是2倍扩容。为什么?因为均摊下来,每次插入的时间复杂度是O(1)。你想想看,如果每次只加1个元素,那每次扩容都要把旧数据全部拷贝一遍,数据量一大就卡死了。

来看一个带扩容的动态数组实现:

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

typedef struct {
    int *data;
    int size;      // 当前元素个数
    int capacity;  // 当前容量
} DynamicArray;

// 初始化
void initArray(DynamicArray *arr, int initCapacity) {
    arr->data = (int *)malloc(initCapacity * sizeof(int));
    arr->size = 0;
    arr->capacity = initCapacity;
}

// 扩容(2倍策略)
void expandArray(DynamicArray *arr) {
    int newCapacity = arr->capacity * 2;
    int *newData = (int *)realloc(arr->data, newCapacity * sizeof(int));
    if (newData == NULL) {
        printf("扩容失败!\n");
        return;
    }
    arr->data = newData;
    arr->capacity = newCapacity;
    printf("扩容:%d -> %d\n", arr->capacity / 2, arr->capacity);
}

// 插入元素
void pushBack(DynamicArray *arr, int value) {
    if (arr->size >= arr->capacity) {
        expandArray(arr);
    }
    arr->data[arr->size++] = value;
}

// 释放
void freeArray(DynamicArray *arr) {
    free(arr->data);
    arr->data = NULL;
    arr->size = 0;
    arr->capacity = 0;
}

int main() {
    DynamicArray arr;
    initArray(&arr, 4);

    for (int i = 0; i < 20; i++) {
        pushBack(&arr, i);
    }

    for (int i = 0; i < arr.size; i++) {
        printf("%d ", arr.data[i]);
    }
    printf("\n");

    freeArray(&arr);
    return 0;
}

避坑指南:

我曾经在项目中直接用realloc,没检查返回值。结果有一次内存不够,realloc返回NULL,原来的指针也丢了——内存泄漏加数据丢失,双重打击。记住:realloc失败时,原内存块不变,但返回NULL。所以一定要用临时变量接收返回值。

18.3 动态二维数组

二维数组的动态实现,说白了就是「数组的数组」。但实现方式有讲究,我见过两种主流做法:

方法一:用指针数组

int **matrix = (int **)malloc(rows * sizeof(int *));
for (int i = 0; i < rows; i++) {
    matrix[i] = (int *)malloc(cols * sizeof(int));
}

这种方法的优点是每行可以有不同的长度(锯齿数组)。缺点是内存不连续,访问时缓存命中率低。我在做图像处理时吃过这个亏——数据量大时,非连续内存的遍历速度能差好几倍。

方法二:用一维数组模拟二维

int *matrix = (int *)malloc(rows * cols * sizeof(int));
// 访问 matrix[i][j] 等价于 matrix[i * cols + j]

这种方法内存连续,访问效率高。我个人的习惯是:只要行列固定,就用一维模拟二维。代码稍微绕一点,但性能好很多。

来看一个完整的动态二维数组例子:

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

int main() {
    int rows = 3, cols = 4;

    // 方法一:指针数组
    int **arr1 = (int **)malloc(rows * sizeof(int *));
    for (int i = 0; i < rows; i++) {
        arr1[i] = (int *)malloc(cols * sizeof(int));
    }

    // 赋值
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            arr1[i][j] = i * cols + j;
        }
    }

    // 释放(注意顺序:先释放行,再释放列指针)
    for (int i = 0; i < rows; i++) {
        free(arr1[i]);
    }
    free(arr1);

    // 方法二:一维模拟二维
    int *arr2 = (int *)malloc(rows * cols * sizeof(int));
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            arr2[i * cols + j] = i * cols + j;
        }
    }
    free(arr2);

    return 0;
}

注意:

释放二维数组时,顺序很重要。先释放每一行的内存,再释放行指针数组。反过来会怎么样?嗯,你试试就知道了——内存泄漏没跑了。

知识体系总览

下面这张图把本章的核心逻辑串起来了,建议你多看几遍:

动态数组 malloc 实现 三步走: ① malloc 申请 ② 像数组一样使用 ③ free 释放 扩容策略 三种策略: ① 固定增量 ② 倍数扩容(推荐) ③ 指数衰减 realloc 要检查返回值 动态二维数组 两种实现: ① 指针数组 ② 一维模拟二维 释放顺序不能乱 核心:malloc + realloc + free,三剑客缺一不可

动态数组这东西,说难不难,说简单也不简单。关键是要理解内存管理的本质——你向系统借了一块地,用完了得还回去。不还?系统会找你算账的(内存泄漏)。还错了?程序直接崩溃(野指针)。

我个人建议,刚开始学的时候,每写一个malloc,就立刻写对应的free。养成习惯后,你会发现动态数组其实比静态数组灵活得多,也强大得多。


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