一、为什么需要动态内存?
内存分两大类:栈区内存、堆区内存
栈区(自动内存)
变量:局部变量、数组,写在{}里面 特点:
✅ 自动分配、自动回收,不用手动释放
❌ 编译时代码就固定大小,运行中不能扩容
❌ 生命周期固定,出了大括号 / 函数,内存立刻回收
比喻:宿舍,房间大小固定,离校自动退房。
堆区(动态内存)
由程序员手动申请、手动释放 特点:
✅ 运行的时候才确定空间大小,可以扩容
✅ 生命周期自己掌控,不调用 free 就一直存在
❌ 用完必须手动释放,不然会内存泄漏
比喻:租房,你自己决定租多大、什么时候退房。
一句话总结:栈内存大小固定、自动回收;堆内存灵活,程序员全权管理。
二、4 个核心动态内存函数(都在 stdlib.h 头文件)
1. malloc —— 在堆上申请一块内存
函数原型:
void* malloc(size_t size);参数 size:想要申请多少字节 返回值: 申请成功:返回堆空间第一个字节的地址 申请失败(内存不够):返回 NULL 空指针
重点:void*通用指针,不知道你存什么类型,必须强制转换成对应类型指针才能使用 malloc 开辟的内存不会初始化,里面是随机垃圾值 ⚠️ malloc 返回值一定要判 NULL,不判断会空指针访问崩溃;申请失败要终止后续操作
示例:
//申请40字节,存10个int int* p = (int*)malloc(10 * sizeof(int)); if(p == NULL) { printf("内存申请失败\n"); return 0; }2. free —— 释放堆内存,还给操作系统
函数原型:
void free(void* ptr);参数 ptr:malloc/calloc/realloc 返回的堆内存起始地址
规则: ✅ 只能释放堆区内存,不能 free 栈变量地址 ✅ 只能传最开始 malloc 拿到的首地址,不能传偏移后的地址(p++ 之后不能 free) ✅ 同一块内存不能多次 free,程序崩溃
好习惯:free 之后,把指针赋值 NULL,消除野指针
free(p); p = NULL;3. calloc —— 申请内存 + 自动初始化为 0
函数原型:
void* calloc(size_t num, size_t size);参数:num 元素个数,size 每个元素占多少字节
和 malloc 区别: malloc:申请 size 字节,内容随机垃圾值 calloc:申请 num*size 字节,全部字节自动置 0
适合场景:申请数组,需要初始值全部是 0 的时候
//申请10个int,全部初始化为0 int* p = (int*)calloc(10, sizeof(int));4. realloc —— 对已经申请好的堆内存扩容 / 缩容
函数原型:
void* realloc(void* ptr, size_t new_size);参数:ptr 原来堆内存地址;new_size 想要的新总字节数
扩容分 3 种情况:
- 原内存后面还有足够连续空闲空间 → 直接向后拓展,返回原来的地址
- 后面空间不够 → 在堆里找一块全新连续空间,拷贝旧数据,释放旧空间,返回新地址
- 内存不足,找不到空间 → 返回 NULL,原来内存不会释放
⚠️ 不要直接写p = realloc(p,新大小),扩容失败会把 p 覆盖成 NULL,丢失原内存地址,造成内存泄漏。 正确写法:用临时指针接收
int* tmp = realloc(p, 20 * sizeof(int)); if(tmp != NULL) { p = tmp; }补充:
realloc(ptr,0):释放 ptr 指向内存,返回值视编译器而定realloc(NULL, size)等价于malloc(size)
三、2 个致命问题:野指针 + 内存泄漏
1. 野指针
定义:指针存了地址,但是这片内存没有权限访问 产生原因:
- 函数返回栈区局部变量地址。函数结束栈内存回收,地址失效。
- free 释放堆内存之后,指针没有置 NULL,指针还保留旧地址。
- 指针没有初始化,随机垃圾地址。
危害:访问野指针 → 程序崩溃、乱码。 解决:free 后p=NULL;指针使用前判空。
2. 内存泄漏
定义:堆内存申请了,用完没有 free,并且把保存首地址的指针弄丢了,这块内存永远无法释放,一直占用资源。
产生场景:
- 保存堆首地址的指针被覆盖,原来 malloc 的地址丢失
- 函数内 malloc,没有 free,保存地址的局部变量出函数销毁
补充知识点:程序正常退出时,操作系统会回收该程序全部堆内存。内存泄漏的危害主要体现在长时间持续运行的程序;短程序直接退出,泄漏影响不大。 现象:程序越跑占内存越高,长时间运行直接卡死。
四、高频易错代码分析(面试必考)
易错 1:传一级指针,函数内 malloc,外部拿不到地址
void GetMemory(char* p) { p = (char*)malloc(100); } //调用 char* str = NULL; GetMemory(str); //值传递:调用函数时,把str的值拷贝一份给形参p,函数内部修改副本p,不会影响外面原始str变量 strcpy(str, "hello"); //str依旧是NULL,崩溃✅ 正确写法:传二级指针
void GetMemory(char** p) { *p = (char*)malloc(100); } //调用 char* str = NULL; GetMemory(&str);易错 2:返回栈数组地址
char* GetMemory(void) { char p[] = "hello world"; //栈数组 return p; //函数结束,p内存回收,返回野指针 ❌错误 }区分:
char* GetMemory(void) { char* p = "hello world"; //字符串字面量放在只读常量区,生命周期是整个程序 return p; //✅可以返回 }一句话总结:char p[]是栈数组;char* p指向常量区字符串,二者内存位置完全不同。
易错 3:free 之后继续使用内存
char* str = malloc(100); strcpy(str, "hello"); free(str); if(str != NULL) { strcpy(str,"world"); //内存已经释放,非法访问 }free 只是归还内存给操作系统,指针变量里地址不会自动清空,需要手动str=NULL。
五、动态内存使用规范(记住这几条,少踩坑)
- malloc/calloc/realloc 的返回值必须判断是否为 NULL
- free 只能释放堆内存,传入的必须是最原始 malloc 首地址
- free 之后立刻将指针置 NULL,防止野指针
- realloc 扩容用临时指针接收,不要直接覆盖原指针
- 堆内存使用完毕,一定要 free,防止内存泄漏
- 不要返回函数内部栈局部变量的地址
六、栈内存 vs 堆内存 对比总结表
表格
| 项目 | 栈内存 | 堆内存 |
|---|---|---|
| 申请方式 | 定义局部变量、数组自动分配 | malloc/calloc/realloc 手动申请 |
| 大小 | 编译时固定,运行不可变;容量很小(一般几 MB),超大数组会栈溢出 | 运行时确定,可以 realloc 扩容;空间大,受系统可用内存限制 |
| 初始化 | 局部变量默认随机垃圾值 | malloc 随机;calloc 全部置 0 |
| 释放时机 | 出 {} / 函数结束自动回收 | 程序员调用 free 手动释放 |
| 生命周期 | 随作用域结束销毁 | 直到 free 或者程序结束 |
| 比喻 | 宿舍 | 租房 |
七、常见错误清单(背诵)
- 不检查 malloc 返回值,空指针访问
- free 非堆空间(栈变量地址)
- free 偏移后的指针(p++ 后 free)
- 同一块内存多次 free
- free 之后继续读写这片内存
- realloc 直接赋值给原指针,扩容失败丢失地址
- malloc 后忘记 free,内存泄漏
- 函数返回栈上局部数组地址,产生野指针