C语言 动态内存管理

1、为什么要有动态内存分配

2、malloc和free

3、calloc和realloc

4、常见的动态内存的错误

5、动态内存经典试题分析

6、柔韧性

7、总结C/C++中程序内存区域的划分

为什么要有动态内存分配

  1. 普通数组大小固定,不灵活
  2. 不知道数据数量时,必须用动态内存
  3. 需要扩大 / 缩小时,必须用动态内存
  4. 需要跨函数使用内存时,必须用动态内存

一句话:动态内存让程序 "灵活、省空间、可控"!

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 动态决定 **,这就是柔性数组。

特点:

  1. 必须在结构体最后一个成员
  2. 前面至少有一个成员
  3. 不算在结构体大小里
  4. 大小可以动态分配

举例:

复制代码
// 柔性数组结构体
struct Stu
{
    int n;                // 前面必须有成员
    int arr[];   // 柔性数组(没有大小)
    // 必须放在最后
};

怎么给柔性数组分配空间?

给结构体开辟空间时,顺便给柔性数组一起开!

复制代码
// 给结构体 + 柔性数组 一起开辟空间
struct Stu* ps = (struct Stu*)malloc(
    sizeof(struct Stu)    // 结构体本身大小
    + 10 * sizeof(int)    // 柔性数组:10个int
);

这样:

  • n 占 4 字节
  • arr40 字节(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
  • 容易内存泄漏
相关推荐
say_fall1 小时前
可编程中断控制器8259A工作方式超详细解析
android·开发语言·学习·硬件架构·硬件工程
San813_LDD1 小时前
[QT]《Qt 开发避坑指南:随机数、容器操作与 VS 环境配置》
开发语言·qt
小糯米6011 小时前
C语言 自定义类型:联合和枚举
java·c语言·开发语言
weixin_523185321 小时前
Java基础知识总结(二):JVM内存结构与变量生命周期
java·开发语言·jvm
石山代码1 小时前
Python 进阶学习指南
开发语言·python
xiaoshuaishuai82 小时前
C# 多线程之间对比
java·开发语言·c#
ZC跨境爬虫3 小时前
跟着 MDN 学JavaScript day_9:字符串方法实战挑战与解题思路
开发语言·前端·javascript
青春:一叶知秋4 小时前
【C++】protobuf序列化与反序列化
开发语言·c++
夕除5 小时前
shizhan--10
java·开发语言