第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 如何避免内存泄漏?

我总结了三条铁律:

  1. 谁分配,谁释放——函数内malloc的,函数内free
  2. 成对出现——写malloc时,立刻写对应的free注释
  3. 异常路径也要释放——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 知识体系总览

下面这张图总结了本章的核心内容:

动态内存管理知识体系 四大核心函数 malloc / calloc realloc / free 内存泄漏 检测:Valgrind/mtrace 避免:成对分配释放 动态数组 容量翻倍策略 按需增长 动态结构体 实践要点:检查返回值 → 避免野指针 → 成对释放 → 异常路径处理 核心原则:谁分配谁释放,malloc与free成对出现 避坑:realloc使用临时变量,free后置NULL 动态结构体释放:先内后外

1.7 总结

动态内存管理是C语言的精髓之一。掌握好malloc/calloc/realloc/free这四个函数,你就能灵活地处理各种变长数据。

记住几个关键点:

  • 每次分配都要检查返回值
  • 每次分配都要想好什么时候释放
  • realloc要用临时变量接返回值
  • free后立即置NULL
  • 动态结构体释放时先内后外

这些习惯养成了,内存泄漏就离你远远的。我在实际项目中见过太多因为内存问题导致的线上事故,希望你能从一开始就打好基础。


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