有时候,程序要用的空间大小,得等跑起来才知道——比如用户临时输入要存多少个数字。这时候,就要向内存「借」一块刚刚好的地,用完再还回去。这就是动态内存管理。

内存区域示意图:栈区、堆区、静态区
动态内存住在「堆区」,由程序员用 malloc / calloc / realloc / free 亲自管理

三个"借地"函数

malloc:借一块不打扫的房间

void* malloc(size_t size);   // 申请,但不初始化

int* p = (int*)malloc(n * sizeof(int));
if (p == NULL) { perror("malloc"); return 1; }   // 一定要检查返回值

calloc:借一块还帮你打扫的房间

void* calloc(size_t num, size_t size);   // 申请并初始化为 0
// calloc == malloc + 初始化

realloc:房间不够大,帮你搬家扩容

void* realloc(void* p, size_t size);
// 情况1:原地后面的空间够 → 直接返回新地址
// 情况2:不够 → 把数据拷贝到新地方,返回新地址,释放旧地址

int* p2 = realloc(p1, 10 * sizeof(int));
if (p2 == NULL) { ... }   // 失败时 p1 还活着,别急着丢
p1 = p2;

free 的温柔约定

借了地,用完一定要还——这是和内存之间最重要的约定:

free 只能释放 malloc / calloc / realloc 申请的空间,而且必须传回它最初的起始地址。释放后要把指针手动置成 NULL,才不会变成危险的「野指针」。

六个经典的翻车现场

  • ① 对 NULL 指针解引用
  • ② 对动态开辟空间越界访问
  • ③ 对非动态内存用 free
  • ④ 用 free 释放动态空间的一部分
  • ⑤ 同一块内存释放多次
  • ⑥ 忘了释放 → 内存泄漏

一个踩坑无数的笔试题

void GetMemory(char* p) {
    p = (char*)malloc(100);   // 只改了形参,实参 str 还是 NULL!
}
void Test(void) {
    char* str = NULL;
    GetMemory(str);
    strcpy(str, "hello world");  // 对空指针拷贝 → 崩
}

正确姿势有两个:用二级指针传地址,或直接用返回值把地址带回来。

动态内存管理的可爱插画
借与还之间,是一场温柔又需要小心的约定

柔性数组:结构体的小尾巴

C99 允许结构体的最后一个成员是一个大小未知的数组,它就像一条可以随时伸缩的小尾巴:

struct S {
    int n;
    char c;
    char arr[];   // 柔性数组成员
};

它不占 sizeof,只能通过动态内存分配来使用。好处有两个:一是一次性分配、释放也只需一个 free,二是连续内存访问更快,还减少内存碎片。

✿ 摘要

  • 动态内存住在堆区,由 malloc / calloc / realloc / free 管理。
  • malloc 不初始化,calloc 初始化为 0,realloc 扩容搬家。
  • 使用后必须 free,并置 NULL;free 只认 malloc 家族、只认起始地址。
  • 常见错误:空指针解引用、越界、重复释放、内存泄漏等六种。
  • 柔性数组是结构体尾部的大小可变数组,方便整体分配与释放。

愿当下的花如你所见般美丽