11、结构体数组的动态分配:为结构体数组分配内存、遍历与操作
结构体数组的动态分配,说白了就是让程序在运行时,按需创建一批结构体变量。我刚开始学C语言时,总觉得静态数组够用了——大不了多定义几个元素嘛。直到有一次做学生管理系统,用户说“我班上有50人”,另一个用户说“我公司有2000人”……嗯,静态数组根本没法玩。动态分配,才是真正的“按需分配”。
11.1 为什么需要动态分配结构体数组?
静态数组的大小在编译时就固定了。你写 struct Student stu[100],那就只能存100个学生。多了存不下,少了浪费内存。你想想看,如果只存了10个学生,那剩下的90个结构体空间就白白占着,多可惜。
动态分配的好处很明显:
- 灵活:运行时根据实际需求决定数组大小
- 节省内存:用多少,申请多少
- 可扩展:不够用了还能用
realloc扩容
核心思想:结构体数组的动态分配,本质上是分配一块连续的内存区域,每个元素都是一个结构体。这块内存的地址,用一个指向结构体的指针来管理。
11.2 分配结构体数组内存
分配结构体数组,最常用的函数是 malloc 和 calloc。我个人习惯用 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 知识体系图
下面这张图总结了结构体数组动态分配的核心流程:
11.7 避坑指南
做动态内存分配,坑不少。我把自己踩过的坑列出来,你遇到了能少走弯路。
- 忘记释放内存:程序跑久了,内存越占越多,最后系统卡死。我曾经有个服务程序跑了三天,内存从10MB涨到2GB……后来发现是循环里分配了内存没释放。
- 越界访问:动态数组的下标从0到n-1,访问
arr[n]就是越界。C语言不检查这个,但后果很严重——可能改写了别的数据,甚至导致段错误。 - realloc 后原指针失效:前面说过了,一定要用临时变量。
- 结构体里有指针成员:如果结构体里包含
char *name这样的指针,分配结构体数组时,只分配了指针本身的空间,没有分配指针指向的字符串空间。需要单独为每个字符串分配内存。
我的习惯:每次 malloc 或 calloc 之后,紧跟着写对应的 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;
}
嗯,到这里,结构体数组的动态分配就讲得差不多了。核心就三件事:分配、操作、释放。每一步都小心一点,别让内存泄漏找上你。