11、结构体数组的动态分配:为结构体数组分配内存、遍历与操作

结构体数组的动态分配,说白了就是让程序在运行时,按需创建一批结构体变量。我刚开始学C语言时,总觉得静态数组够用了——大不了多定义几个元素嘛。直到有一次做学生管理系统,用户说“我班上有50人”,另一个用户说“我公司有2000人”……嗯,静态数组根本没法玩。动态分配,才是真正的“按需分配”。

11.1 为什么需要动态分配结构体数组?

静态数组的大小在编译时就固定了。你写 struct Student stu[100],那就只能存100个学生。多了存不下,少了浪费内存。你想想看,如果只存了10个学生,那剩下的90个结构体空间就白白占着,多可惜。

动态分配的好处很明显:

  • 灵活:运行时根据实际需求决定数组大小
  • 节省内存:用多少,申请多少
  • 可扩展:不够用了还能用 realloc 扩容

核心思想:结构体数组的动态分配,本质上是分配一块连续的内存区域,每个元素都是一个结构体。这块内存的地址,用一个指向结构体的指针来管理。

11.2 分配结构体数组内存

分配结构体数组,最常用的函数是 malloccalloc。我个人习惯用 calloc,因为它会把分配的内存初始化为0,避免一些“脏数据”问题。

来看一个例子:

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

// 定义一个学生结构体
typedef struct {
    int id;          // 学号
    char name[32];   // 姓名
    float score;     // 成绩
} Student;

int main() {
    int n = 5;  // 假设要管理5个学生
    Student *stuArr;

    // 动态分配结构体数组
    stuArr = (Student *)malloc(n * sizeof(Student));
    if (stuArr == NULL) {
        printf("内存分配失败!\n");
        return -1;
    }

    // 使用完毕后释放
    free(stuArr);
    return 0;
}

这里要注意:malloc 的参数是 n * sizeof(Student),而不是 n * sizeof(Student *)。我见过不少新手在这里写错,结果分配的内存根本不够用。

警告malloc 返回的是 void *,需要强制类型转换为 Student *。虽然C语言允许隐式转换,但显式转换能让代码更清晰,也方便C++编译器兼容。

11.3 遍历结构体数组

遍历动态分配的结构体数组,和遍历静态数组没什么两样。因为动态分配的内存是连续的,所以可以用下标访问,也可以用指针偏移。

// 初始化数据
for (int i = 0; i < n; i++) {
    stuArr[i].id = 1001 + i;
    sprintf(stuArr[i].name, "学生_%d", i + 1);
    stuArr[i].score = 60.0f + i * 5.0f;
}

// 遍历输出
printf("学生信息列表:\n");
for (int i = 0; i < n; i++) {
    printf("学号: %d, 姓名: %s, 成绩: %.1f\n",
           stuArr[i].id, stuArr[i].name, stuArr[i].score);
}

用指针方式遍历也是一样的效果:

Student *p = stuArr;
for (int i = 0; i < n; i++, p++) {
    printf("学号: %d, 姓名: %s, 成绩: %.1f\n",
           p->id, p->name, p->score);
}

我个人更推荐用下标方式,可读性更好。指针方式虽然效率略高一点点,但容易写错——尤其是 p++p->id 混用时,脑子得转个弯。

11.4 操作结构体数组:查找、修改、排序

动态分配的结构体数组,支持所有常规操作。这里我挑几个常用的说说。

11.4.1 查找某个学生

int findById(Student *arr, int n, int targetId) {
    for (int i = 0; i < n; i++) {
        if (arr[i].id == targetId) {
            return i;  // 返回下标
        }
    }
    return -1;  // 没找到
}

11.4.2 修改成绩

void updateScore(Student *arr, int n, int id, float newScore) {
    int idx = findById(arr, n, id);
    if (idx != -1) {
        arr[idx].score = newScore;
        printf("学号 %d 的成绩已更新为 %.1f\n", id, newScore);
    } else {
        printf("未找到学号 %d 的学生\n", id);
    }
}

11.4.3 按成绩排序

#include <stdlib.h>

int compareByScore(const void *a, const void *b) {
    Student *sa = (Student *)a;
    Student *sb = (Student *)b;
    if (sa->score > sb->score) return -1;
    if (sa->score < sb->score) return 1;
    return 0;
}

// 使用 qsort 排序
qsort(stuArr, n, sizeof(Student), compareByScore);

这里用 qsort 排序,效率高,代码也简洁。我在项目中经常用这个套路——定义一个比较函数,然后一行 qsort 搞定。

小技巧qsort 的比较函数返回负数、0、正数,分别表示第一个参数小于、等于、大于第二个参数。想升序还是降序,全靠比较函数里的逻辑控制。

11.5 动态扩容:用 realloc 增加数组大小

实际项目中,你一开始可能不知道到底要存多少数据。比如从文件里读学生信息,读着读着发现不够了。这时候 realloc 就派上用场了。

// 假设原来有 n 个学生,现在要扩容到 newSize
int newSize = n * 2;
Student *temp = (Student *)realloc(stuArr, newSize * sizeof(Student));
if (temp == NULL) {
    printf("扩容失败!\n");
    // 注意:此时 stuArr 仍然有效,不能直接覆盖
} else {
    stuArr = temp;
    n = newSize;
    printf("数组已扩容到 %d 个元素\n", n);
}

重要提醒realloc 可能会移动内存块。如果移动了,原来的指针就失效了。所以一定要用临时变量接收返回值,检查成功后再赋值给原指针。我曾经因为直接 stuArr = realloc(...) 导致内存泄漏,排查了半天……

11.6 知识体系图

下面这张图总结了结构体数组动态分配的核心流程:

结构体数组动态分配核心流程 1. 定义结构体类型 typedef struct { ... } Student; 2. 分配内存 malloc / calloc 3. 初始化数据 遍历赋值 4. 操作数组 查找 / 修改 / 排序 5. 动态扩容 realloc 6. 释放内存 free 可选 注意:每一步都要检查返回值,避免操作空指针或野指针 ⚠ 忘记 free 会导致内存泄漏!

11.7 避坑指南

做动态内存分配,坑不少。我把自己踩过的坑列出来,你遇到了能少走弯路。

  • 忘记释放内存:程序跑久了,内存越占越多,最后系统卡死。我曾经有个服务程序跑了三天,内存从10MB涨到2GB……后来发现是循环里分配了内存没释放。
  • 越界访问:动态数组的下标从0到n-1,访问 arr[n] 就是越界。C语言不检查这个,但后果很严重——可能改写了别的数据,甚至导致段错误。
  • realloc 后原指针失效:前面说过了,一定要用临时变量。
  • 结构体里有指针成员:如果结构体里包含 char *name 这样的指针,分配结构体数组时,只分配了指针本身的空间,没有分配指针指向的字符串空间。需要单独为每个字符串分配内存。

我的习惯:每次 malloccalloc 之后,紧跟着写对应的 free 代码。哪怕还没写中间的逻辑,先把释放的架子搭好。这样就不容易忘了。

11.8 完整示例:学生成绩管理系统(片段)

最后给一个完整的示例,把上面讲的东西串起来:

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

typedef struct {
    int id;
    char name[32];
    float score;
} Student;

int main() {
    int n = 3;
    Student *stu = (Student *)calloc(n, sizeof(Student));
    if (!stu) { printf("分配失败\n"); return -1; }

    // 初始化
    stu[0] = (Student){1001, "张三", 78.5f};
    stu[1] = (Student){1002, "李四", 92.0f};
    stu[2] = (Student){1003, "王五", 65.5f};

    // 遍历
    for (int i = 0; i < n; i++) {
        printf("%d %s %.1f\n", stu[i].id, stu[i].name, stu[i].score);
    }

    // 扩容到5个
    Student *tmp = (Student *)realloc(stu, 5 * sizeof(Student));
    if (!tmp) { free(stu); return -1; }
    stu = tmp;
    stu[3] = (Student){1004, "赵六", 88.0f};
    stu[4] = (Student){1005, "钱七", 72.0f};

    // 按成绩排序
    qsort(stu, 5, sizeof(Student), compareByScore);

    // 释放
    free(stu);
    return 0;
}

嗯,到这里,结构体数组的动态分配就讲得差不多了。核心就三件事:分配、操作、释放。每一步都小心一点,别让内存泄漏找上你。

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