10、结构体的动态分配:为结构体变量分配内存、结构体指针的使用
结构体这东西,说白了就是C语言里用来“打包数据”的容器。你想想看,一个学生有学号、姓名、成绩,如果每个字段都单独声明变量,那代码写起来得多乱?结构体就是干这个的。但问题来了——如果学生数量不确定呢?如果要在函数之间传递结构体呢?这时候,动态分配就派上用场了。
10.1 为什么结构体需要动态分配?
我刚开始学C语言那会儿,总觉得结构体用静态声明就够了。直到有一次做项目,要处理一个未知数量的设备节点——用户可能插3个设备,也可能插300个。如果我用静态数组,要么浪费内存,要么不够用。嗯,这就是动态分配的价值所在。
静态分配的结构体,生命周期是固定的。要么是全局的,要么是局部的。但动态分配的结构体,你可以在运行时决定它的生死。说白了,就是“按需分配,用完即还”。
10.2 为结构体变量分配内存
先看一个最简单的例子。假设我们有一个学生结构体:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
int id;
char name[50];
float score;
} Student;
int main() {
// 为单个结构体变量分配内存
Student *p = (Student *)malloc(sizeof(Student));
if (p == NULL) {
printf("内存分配失败!\n");
return -1;
}
// 使用结构体指针访问成员
p->id = 1001;
strcpy(p->name, "张三");
p->score = 89.5;
printf("学号:%d,姓名:%s,成绩:%.1f\n", p->id, p->name, p->score);
free(p);
return 0;
}
这里有个细节我特别想强调:sizeof(Student) 计算的是整个结构体的大小,包括可能的对齐填充字节。我在项目中遇到过有人直接用 sizeof(Student*) 来分配——那分配的是指针本身的大小,不是结构体的大小。这种bug很难查,因为程序可能偶尔能跑,但数据会被覆盖。
malloc(sizeof(Student*)) 代替 malloc(sizeof(Student))。结果结构体里的字符串总是莫名其妙地被截断。查了两天才发现是内存分配少了。记住:sizeof(结构体类型) 和 sizeof(结构体指针) 完全是两码事。
10.3 结构体指针的使用方式
结构体指针有两种访问成员的方式:箭头(->)和点(.)。很多人刚学时容易搞混,其实规则很简单:
- 如果有一个结构体指针
p,用p->成员名 - 如果有一个结构体变量
s,用s.成员名 - 如果对指针解引用,也可以写成
(*p).成员名,但没人这么写,太啰嗦
我个人习惯是:只要涉及动态分配,一律用指针 + 箭头。这样代码读起来一目了然——看到箭头就知道这是个指针,背后可能是动态内存。
10.4 动态分配结构体数组
实际项目中,更常见的是分配一组结构体。比如要管理一个班级的学生:
int n = 5; // 假设有5个学生
Student *class = (Student *)malloc(n * sizeof(Student));
if (class == NULL) {
// 处理错误
}
// 像数组一样使用
for (int i = 0; i < n; i++) {
class[i].id = 2000 + i;
sprintf(class[i].name, "学生_%d", i + 1);
class[i].score = 60.0 + rand() % 40;
}
// 释放
free(class);
注意这里用的是 class[i].id,而不是 class[i]->id。为什么?因为 class 是指向数组首元素的指针,class[i] 是数组中的第 i 个结构体变量,不是指针。嗯,这个细节容易搞混,我建议你写代码时多留意。
10.5 结构体嵌套与动态分配
结构体里还能嵌套结构体,甚至嵌套指针。比如:
typedef struct {
int year;
int month;
int day;
} Date;
typedef struct {
int id;
char *name; // 字符串用指针,动态分配
Date birthday; // 嵌套结构体,静态成员
float *scores; // 成绩数组,动态分配
} StudentEx;
这种结构体的分配就复杂了。你不能只 malloc 一个 StudentEx 就完事,还得给内部的指针成员分配内存:
StudentEx *p = (StudentEx *)malloc(sizeof(StudentEx));
p->name = (char *)malloc(50 * sizeof(char));
p->scores = (float *)malloc(10 * sizeof(float));
// 使用...
strcpy(p->name, "李四");
p->scores[0] = 95.0;
// 释放时顺序很重要:先释放内部,再释放外部
free(p->scores);
free(p->name);
free(p);
free(p) 再释放内部指针,结果 p->name 变成了野指针,程序直接崩溃。释放顺序搞反了,就是典型的“悬空指针”问题。
10.6 结构体指针作为函数参数
把结构体传给函数,有两种方式:传值或传指针。传值会拷贝整个结构体,如果结构体很大,性能开销不小。传指针只拷贝4或8个字节,效率高得多。
// 传指针方式(推荐)
void print_student(const Student *p) {
printf("ID: %d, Name: %s, Score: %.1f\n",
p->id, p->name, p->score);
}
// 传值方式(不推荐,尤其结构体很大时)
void print_student_by_value(Student s) {
printf("ID: %d, Name: %s, Score: %.1f\n",
s.id, s.name, s.score);
}
我个人习惯是:只要结构体超过两个成员,一律用指针传参。你想想看,一个结构体如果有几十个字段,每次传值都要在栈上拷贝几十个字节,何必呢?
10.7 知识体系图
下面这张图总结了结构体动态分配的核心逻辑:
10.8 常见陷阱与最佳实践
| 陷阱 | 说明 | 解决方案 |
|---|---|---|
| 忘记释放 | malloc 后没有 free,造成内存泄漏 | 每次 malloc 后,在合适的时机配对 free |
| 重复释放 | 对同一指针 free 两次,导致未定义行为 | free 后立即将指针置为 NULL |
| 野指针 | free 后继续使用指针访问内存 | free 后置 NULL,使用前检查 |
| 分配大小错误 | sizeof(指针) 代替 sizeof(结构体) | 始终用 sizeof(类型) 或 sizeof(*指针) |
| 嵌套结构体释放顺序 | 先释放外层,导致内层指针悬空 | 先释放内层成员,再释放外层结构体 |
malloc 的时候,我就在同一行或者紧接着写注释 // TODO: 记得 free。等代码写完,再回头检查每个 malloc 是否都有对应的 free。这招虽然土,但很管用。
结构体的动态分配,说白了就是“手动管理内存的生命周期”。你分配了,就要负责释放。你用了指针,就要确保它指向有效内存。这些规则听起来简单,但实际项目中稍不留神就会踩坑。嗯,多写多练,慢慢就形成肌肉记忆了。
公众号:蓝海资料掘金营,微信deep3321