第1章:动态内存管理——堆上的艺术
说实话,很多C语言学习者学到指针和数组就卡住了。但真正让C语言区别于其他语言的,是你能亲手管理内存。今天我们就来聊聊动态内存管理——说白了,就是向操作系统要一块内存,用完了再还回去。
我刚开始做嵌入式开发时,总觉得动态内存分配是“高级功能”,单片机嘛,静态分配就够了。直到有一次做一个需要动态解析数据包的项目,我才发现:不会动态内存管理,很多场景根本玩不转。
1.1 为什么需要动态内存?
你想想看,写程序时经常遇到这种情况:
- 不知道用户会输入多长的名字
- 不知道文件里有多少条记录
- 不知道网络包会来多大
静态数组能解决吗?能,但很浪费。你开个char name[1024],大部分时候只用几十字节。或者更糟——开小了,程序直接崩。
动态内存就是解决这个问题的。运行时决定大小,用多少要多少。
1.2 四大金刚:malloc / calloc / realloc / free
这四个函数是动态内存管理的核心。我习惯把它们叫做“借、清、调、还”。
1.2.1 malloc —— 借内存
void* malloc(size_t size);
malloc从堆上申请一块连续内存,返回首地址。注意:它不初始化,里面是垃圾值。
重要:malloc返回的是void*,需要强制类型转换。在C语言中可以不转,但我建议你转——代码更清晰。
int* arr = (int*)malloc(10 * sizeof(int));
if (arr == NULL) {
// 处理内存分配失败
}
我在项目中遇到过:有个同事忘了检查malloc返回值,结果在内存不足时直接访问NULL指针,程序崩溃了整整两天才找到原因。嗯,从那以后我每次malloc必检查返回值。
1.2.2 calloc —— 借并清零
void* calloc(size_t num, size_t size);
calloc和malloc的区别:它会把分配的内存全部初始化为0。参数也分成了两个:元素个数和每个元素大小。
int* arr = (int*)calloc(10, sizeof(int));
// 等价于 malloc + memset(arr, 0, 10*sizeof(int))
我个人习惯:如果分配后马上要赋值,用malloc;如果分配后需要默认值0,用calloc。省得自己再写memset。
1.2.3 realloc —— 调整大小
void* realloc(void* ptr, size_t new_size);
realloc用来调整已分配内存的大小。它可能原地扩展,也可能搬到一个新地址。
注意:realloc返回的新地址可能和原来不同。千万不要这样写:
ptr = realloc(ptr, new_size); // 错误!如果失败,ptr就丢了
正确做法:
int* new_ptr = (int*)realloc(ptr, new_size);
if (new_ptr != NULL) {
ptr = new_ptr;
} else {
// 处理失败,ptr仍然有效
}
我曾经犯过这个错:realloc失败返回NULL,直接把原指针覆盖了,那块内存再也找不回来。那叫一个惨啊……
1.2.4 free —— 还内存
void free(void* ptr);
free把内存还给系统。注意:只能free malloc/calloc/realloc返回的指针,不能free栈上的地址。
好习惯:free之后立即把指针置为NULL,防止野指针。
free(arr);
arr = NULL;
1.3 内存泄漏:看不见的杀手
内存泄漏就是:你借了内存,忘了还。程序跑得越久,占用的内存越多,最后系统扛不住。
嵌入式系统内存本来就小,泄漏一次可能就死机了。我调试过一个设备,运行72小时后必死,最后发现是一个循环里malloc了没free。
1.3.1 如何检测内存泄漏?
工欲善其事,必先利其器。我常用的方法:
- Valgrind:Linux下的神器,直接告诉你哪行代码泄漏了
- mtrace:glibc自带的内存跟踪工具
- 自己封装:重写malloc/free,记录分配和释放的配对情况
// 简单的内存泄漏检测宏
#define DEBUG_MALLOC(size) debug_malloc(size, __FILE__, __LINE__)
#define DEBUG_FREE(ptr) debug_free(ptr, __FILE__, __LINE__)
void* debug_malloc(size_t size, const char* file, int line) {
void* ptr = malloc(size);
printf("[MALLOC] %s:%d -> %p, size=%zu\n", file, line, ptr, size);
return ptr;
}
void debug_free(void* ptr, const char* file, int line) {
printf("[FREE] %s:%d -> %p\n", file, line, ptr);
free(ptr);
}
1.3.2 如何避免内存泄漏?
我总结了三条铁律:
- 谁分配,谁释放——函数内malloc的,函数内free
- 成对出现——写malloc时,立刻写对应的free注释
- 异常路径也要释放——return之前,别忘了free
避坑指南:我曾经在错误处理分支里忘了free,导致一个服务跑了一周后内存耗尽。从那以后,我写代码时会在每个return之前检查所有已分配的内存是否都已释放。
1.4 动态数组的实现
静态数组大小固定,动态数组可以按需增长。说白了,就是封装一个结构体,里面存指针、当前大小和容量。
typedef struct {
int* data; // 数据指针
size_t size; // 当前元素个数
size_t capacity; // 当前容量
} DynamicArray;
// 初始化
void da_init(DynamicArray* da) {
da->data = NULL;
da->size = 0;
da->capacity = 0;
}
// 添加元素
void da_push_back(DynamicArray* da, int value) {
if (da->size >= da->capacity) {
// 容量翻倍
size_t new_cap = da->capacity == 0 ? 4 : da->capacity * 2;
int* new_data = (int*)realloc(da->data, new_cap * sizeof(int));
if (new_data == NULL) {
// 处理失败
return;
}
da->data = new_data;
da->capacity = new_cap;
}
da->data[da->size++] = value;
}
// 释放
void da_destroy(DynamicArray* da) {
free(da->data);
da->data = NULL;
da->size = 0;
da->capacity = 0;
}
容量翻倍是个经典策略。为什么是2倍?太小了频繁realloc,太大了浪费内存。2倍是个不错的折中。
1.5 动态结构体
结构体里也可以包含动态成员。最常见的场景:字符串、变长数组。
typedef struct {
int id;
char* name; // 动态分配
int* scores; // 动态分配
} Student;
Student* create_student(int id, const char* name, int score_count) {
Student* s = (Student*)malloc(sizeof(Student));
if (s == NULL) return NULL;
s->id = id;
s->name = (char*)malloc(strlen(name) + 1);
if (s->name == NULL) {
free(s);
return NULL;
}
strcpy(s->name, name);
s->scores = (int*)calloc(score_count, sizeof(int));
if (s->scores == NULL) {
free(s->name);
free(s);
return NULL;
}
return s;
}
void destroy_student(Student* s) {
if (s == NULL) return;
free(s->name);
free(s->scores);
free(s);
}
注意:释放动态结构体时,要先释放内部成员,再释放结构体本身。顺序反了,内部成员就找不到了。
1.6 知识体系总览
下面这张图总结了本章的核心内容:
1.7 总结
动态内存管理是C语言的精髓之一。掌握好malloc/calloc/realloc/free这四个函数,你就能灵活地处理各种变长数据。
记住几个关键点:
- 每次分配都要检查返回值
- 每次分配都要想好什么时候释放
- realloc要用临时变量接返回值
- free后立即置NULL
- 动态结构体释放时先内后外
这些习惯养成了,内存泄漏就离你远远的。我在实际项目中见过太多因为内存问题导致的线上事故,希望你能从一开始就打好基础。
公众号:蓝海资料掘金营,微信deep3321