1、为什么要有动态内存分配
2、malloc和free
3、calloc和realloc
4、常见的动态内存的错误
5、动态内存经典试题分析
6、柔韧性
7、总结C/C++中程序内存区域的划分
为什么要有动态内存分配
- 普通数组大小固定,不灵活
- 不知道数据数量时,必须用动态内存
- 需要扩大 / 缩小时,必须用动态内存
- 需要跨函数使用内存时,必须用动态内存
一句话:动态内存让程序 "灵活、省空间、可控"!
malloc = 向系统 "申请" 一块空间free = 用完后 "归还" 这块空间只申请不归还 = 内存泄漏(房间一直占着不用)
malloc的语法 :
void* malloc (size_t size);
size:要分配的内存块的字节数
举例
// 申请 40 字节(存放 10 个 int)
int* p = (int*)malloc(10 * sizeof(int));
malloc的功能:向内存的堆区申请一块连续可用的空间,并返回指向这块空间的起始地址
返回值:
如果开辟成功,则返回这块空间的起始地址
如果开辟失败(如果系统内存不足),则返回一个NULL指针,因此malloc的返回值一定要做检查
返回值的类型是void*,所以malloc函数斌不知道开辟空间的类型,集体在使用的时候使用者自己来决定
注意事项:如果参数size为0,malloc的行为是标准是未定义的,取决于编译器
free
作用:把 malloc 申请的空间 还给操作系统
语法: void free (void* ptr);
举例
free(指针); free(p);
参数:
ptr:指向要释放的内存块的指针
如果参数ptr指向的空间不是动态开辟的,那么free函数的行为是未定义的
如果参数ptr是NULL指针,则函数什么事都不做
malloc和free的声明都在stdlib.h头文件中
最经典的生活例子(秒懂)
你要开房间住酒店:
- malloc = 去前台开房间
- p = 房卡
- free(p) = 退房
规则:
- 只开不退 → 占着房间不让别人用(内存泄漏)
- 没开就退 → 崩溃
- 退了还继续用房卡 → 非法访问(野指针)
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL)
{
perror("malloc");
return 1;
}
int i = 0;
for (i = 0;i < 10;i++)
{
*(p + i) = i + 1;
}
free(p);
p = NULL; // 避免成为野指针
return 0;
}
calloc
语法:
void* calloc (size_t num, size_t size);
malloc与calloc的区别:
1、参数不同 2、calloc在返回内存空间的地址之前,会初始化内存为0
你想把空间开辟好之后就初始化为0,就使用calloc,不想就使用malloc
calloc = malloc + memset
realloc
realloc函数的出现让动态内存空间管理更加灵活
有时候我们发现过去申请的空间太小了,有的时候我们又会觉得申请的空间太大了,那么未来合理的使用内存,我们一定会对内存空间的大小做灵活的调整,那么realloc函数就可以做到
语法: void* realloc (void* ptr, size_t size);
realloc(原来的指针, 新的大小);
功能:ealloc = 调整动态内存大小(变大 / 变小)就是:原来的空间不够用了,帮我重新开一块更大的!
参数:
ptr是要调整的内存空间的起始地址,如果ptr是NULL指针,realloc函数的功能类似于malloc函数
size调整之后的新大小,单位是字节
返回值:
成功:返回一个指向重新分配的内存块的void*类型指针。这个指针可能与原来的指针不同
失败:如果内存重新分配失败,返回NULL,并且原来的内存空间块保持不变
你先用 malloc 申请了空间:
int* p = malloc(5 * sizeof(int));
结果你发现:只能存 5 个数字,不够用了!我要存 10 个!
普通数组做不到,malloc 也不能直接变大。
这时候必须用:
realloc
那么接下来就因该这样写:
int* new_p = realloc(p, 10 * sizeof(int));
realloc 3 个关键点
① realloc 不是额外开空间,是 "调整 / 替换"
不是在原来后面加,而是重新找一块连续空间。
② 一定要用新指针接收!
int* new_p = realloc(...)
如果直接写:
p = realloc(p, ...);
万一扩容失败,返回 NULL ,你原来的地址就丢了!数据全没了!
③ realloc 会自动保留原来的数据
你不用手动拷贝,它会自动搬过去。
realloc注意事项
realloc在调整内存空间大小的时候,存在两种情况
情况一:
原有空间之后有足够大的空间,要扩展的内存就直接在原来内存之后直接追加,原来空间的数据不发生变化,最终返回的地址还是旧地址
情况二:
原有空间之后没有足够大的空间,会在内存的堆区寻找新的满足要求的空间,返回新的起始地址
对动态开辟空间的越界访问与非动态空间的非法访问的区别
- 非动态数组越界(栈区) :踩坏栈 ,通常直接崩溃,错得很明显。
- 动态内存越界(堆区) :踩坏堆 ,当时不崩,后面崩溃,查错极难!
栈越界(非动态数组)
你在自己房间 里乱砸→ 砸到墙,房子立刻塌→ 当场发现错误
堆越界(动态内存)
你在小区公共区域 乱砸→ 当时没事→ 结果破坏了地基→ 过了一会儿,整栋楼塌了→ 你根本不知道是自己刚才踩坏的
同一块动态内存多次释放(重复 free)
我给你讲最核心、最致命、一踩就崩的原理,超级直白!
同一块动态内存,只能 free 一次! free 两次 / 多次 → 直接崩溃!
常见的动态内存错误
- 不检查 NULL
- 越界访问
- free 栈空间
- 重复 free
- 指针移动后 free
- 忘记 free(内存泄漏)
- 使用已释放空间(野指针)
非动态内存 free
int a = 10;
int* p = &a;
free(p); // ❌ 栈上的空间不能 free
后果 :崩溃解决:只有 malloc/calloc/realloc 的空间才能 free
free 时只释放一部分
int* p = (int*)malloc(20);
p++; // 指针移动了
free(p); // ❌ 不再指向起始位置
后果 :崩溃、内存泄漏解决:不要修改动态内存的起始地址
动态内存忘记 free(内存泄漏)
void test() {
int* p = (int*)malloc(20);
// 没 free
}
int main() {
while(1) test(); // 内存越占越多,最后卡死
}
后果 :内存越来越少,程序变慢、崩溃解决:malloc 一定要配套 free
柔性数组(Flexible Array)
在结构体最后,放一个 没有固定大小的数组, 它的大小可以用 malloc 动态决定 **,这就是柔性数组。
特点:
- 必须在结构体最后一个成员
- 前面至少有一个成员
- 不算在结构体大小里
- 大小可以动态分配
举例:
// 柔性数组结构体
struct Stu
{
int n; // 前面必须有成员
int arr[]; // 柔性数组(没有大小)
// 必须放在最后
};
怎么给柔性数组分配空间?
给结构体开辟空间时,顺便给柔性数组一起开!
// 给结构体 + 柔性数组 一起开辟空间
struct Stu* ps = (struct Stu*)malloc(
sizeof(struct Stu) // 结构体本身大小
+ 10 * sizeof(int) // 柔性数组:10个int
);
这样:
n占 4 字节arr占 40 字节(10 个 int)- 总共一起连续分配
柔性数组的超级优点
1. 内存连续,访问更快
柔性数组和结构体在同一块连续内存连续内存 = 缓存命中率高 = 速度快
2. 只需要 free 一次
结构体和数组一起释放,方便、不易出错
3. 不浪费空间
要多大开多大,非常灵活
对比:不用柔性数组的写法
struct Stu
{
int n;
int* arr; // 指针
};
// 要 malloc 两次
struct Stu* ps = malloc(sizeof(struct Stu));
ps->arr = malloc(10 * sizeof(int));
// 要 free 两次
free(ps->arr);
free(ps);
缺点:
- 内存不连续
- 要两次 malloc
- 要两次 free
- 容易内存泄漏
