【C 语言】动态内存管理 [进阶篇 _ 复习专用]
![【C语言】动态内存管理 [进阶篇_ 复习专用]](https://static001.geekbang.org/infoq/2c/2c464b2ffe67adb111f6b28695157457.jpeg)
前情提要
本章节就进入 C 语言的核心:深度剖析 C 语言动态内存管理
接下来我们即将进入一个全新的空间,对代码有一个全新的视角~
以下的内容一定会让你对 C 语言有一个颠覆性的认识哦!!!
以下内容干货满满,跟上步伐吧~
💡本章重点
动态内存分配
动态内存函数
常见错误 &经典笔试题
柔性数组
🍞一.动态内存分配
🥐Ⅰ.为什么存在动态内存分配
💡在我们已知的 C 语言对于内存空间的开辟有一个方法:申请栈区开辟开辟
👉如下:
❗特别注意:
对于上述的开辟空间的方式有两个特点:
1️⃣空间开辟大小是
固定的2️⃣ 数组在申明的时候,必须
指定数组的长度,它所需要的内存在编译时分配
✨综上:
这种方式开辟的空间不够
灵活有时候可能需要在程序运行的时候才知道所需的空间,那这种空间的开辟方式就不能满足了
➡️以下,便引出我们的动态内存开辟啦~
🍞二.动态内存函数
🥐Ⅰ.malloc 和 free
💡malloc函数的作用: 向内存的堆区申请开辟一块连续可用的空间
函数的参数: 想要申请的空间大小【单位:
字节】函数的返回值: 返回的是指向这块空间起始地址的指针,指针类型为
void*
➡️函数工作原理:
如果
malloc开辟空间成功,则返回一个指向开辟好空间起始地址的指针如果
malloc开辟空间失败,则返回一个NULL指针
❗有了以上了解,我们可以得出三点:
1️⃣
malloc函数开辟的空间是在堆区上开辟的,且是连续的,这也就是为什么可以类似于访问数组成员的方式使用这块空间2️⃣
malloc开辟成功时返回类型为void*,是因为malloc函数并不知道开辟空间的类型,所以具体类型在使用的时候自己来决定,去强制转换成什么样的类型3️⃣
malloc开辟失败时返回值为NULL,所以我们需要对返回值做出检查,以防非法访问内存的现象出现
💥特别注意:
malloc函数不允许开辟0字节的空间,因为这个属于标准未定义,是无意义的
✨函数存储的位置:
接下来,我们再介绍一下free函数,便可以开始使用动态内存开辟啦~
🥐Ⅱ.free 函数
💡free函数的作用: 用来做动态内存的释放和回收
函数的参数: 输入指向想要释放的空间的起始地址的指针
函数的返回类型:
void*
➡️函数工作原理:
将传入的指针所指向的那块空间释放掉
❗有了以上了解,我们可以得出如下结论:
➡️想要释放指针所指向的那块空间,指针必须是指向空间的
起始地址,否则会产生如下问题:若指针丢失了开辟的空间的起始地址【Eg:指向的是空间中的其它地址……】,释放此空间时会造成
内存泄露(即释放的空间不干净)若没有对开辟的空间进行及时的释放,则也会造成
内存泄露的问题,那这块空间就会一直占用着内存,浪费空间
💥特别注意:
1️⃣如果
参数指向的空间不是动态开辟的,那 free 函数的行为是未定义的2️⃣如果
参数是NULL指针,则函数什么事都不做
✨综上: 对于动态开辟的空间,使用完后记得及时释放~
🥐Ⅲ.malloc 和 free 的使用
💡示例如下:
❗由上,同学们觉得下面这行代码有必要吗?
➡️答案是:有必要的,因为:
当这个指针所指向的空间被释放后,我们应该
主动的将这个指针置为NULL,以防止非法访问内存
💥特别注意:
👉堆区的空间也不是无限制开辟的,是有限的
🥐Ⅳ.calloc 函数
💡calloc函数的作用: 与malloc函数一样,用来动态内存分配
函数的参数:
num为想要开辟地空间个数size为想要开辟的单个空间的大小【单位:字节】函数的返回值: 返回的是指向这块空间起始地址的指针,指针类型为
void*
➡️函数工作原理:
1️⃣与
malloc一样,且传参的时候比malloc函数更加具体,将两个参数分开来填写2️⃣而且会将开辟好的这块空间全部初始化为
0
❗综上,我们可以得出:
与函数
malloc的区别只在于calloc会在返回地址之前,把申请的空间的每个字节初始化为全0
👉举个例子:
🥐Ⅴ.realloc 函数
💡realloc函数的作用: 再原有动态内存开辟的空间基础上,实现对动态开辟内存大小的调整
函数的参数:
ptr为要调整动态内存开辟的内存地址size为调整之后的新大小函数的返回值: 返回的是
调整之后指向这块空间起始地址的指针,指针类型为void*
➡️函数工作原理:
1️⃣
realloc函数的出现让动态内存管理更加灵活2️⃣
realloc函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间
❗综上,我们可以得出点:
realloc在调整内存空间的是存在两种情况:
①原有空间之后有足够大的空间:
realloc便会将扩大的空间在原有空间的基础上,向后追加空间,原来空间的数据不发生变化
②原有空间之后没有足够大的空间: 在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址。【原有的数据会存储在新的空间上,且原有的空间会被
释放】
❗特别注意:
极度不建议拿原有的指针去接收新调整过的空间
因为开辟失败,返回的是
NULL指针,赋给原有指针的话,会把原有的空间地址给覆盖,那原来那块空间便会丢失,找不到 ,会造成内存泄漏的问题
🥯Ⅵ.总结
✨综上:就是开辟动态内存函数啦~
🍞三.常见的动态内存错误
🥐Ⅰ.对 NULL 指针的解引用操作
❗会造成非法访问内存的问题
✨为了防止这种问题出现,应对动态内存开辟后的返回值作判断先
🥐Ⅱ.对动态开辟空间的越界访问
❗会造成非法访问内存的问题
✨为了防止这种问题出现,应对谨慎对照着使用空间
🥐Ⅲ.对非动态开辟内存使用 free 释放
❗该行为是未定义的
✨为避免,可简单理解为free与动态内存开辟来搭配使用即可
🥐Ⅳ.使用 free 释放一块动态开辟内存的一部分 or 忘记释放
❗该行为会造成释放不完全,形成内存泄露的问题
✨所以未避免这种问题,应避免对指针的地址进行操作
🥐Ⅴ.对同一块动态内存多次释放
❗会造成程序报错
✨重复释放的操作就完全没意义啦~
🍞Ⅳ.经典的笔试题
🥐Ⅰ.试题一
❓请问运行Test函数会有什么样的结果?
💡以上程序共犯如下错误:
1️⃣
str传给GetMemory函数的时候,是值传递,所以 GetMemory 函数的形参p是str的一份临时拷贝,所以在函数内部申请的空间的返回值并没有真正赋值给str2️⃣因为
str还是NULL,所以strcpy会执行失败3️⃣当 GetMemory 函数返回之后,形参
p销毁,使得动态内存开辟的 100 个字节的空间存在内存泄露,无法释放
✨为避免这种错误,可以:
在传参的时候传
&str,然后形参部分用二级指针接收即可
🥐Ⅱ.试题二
❓请问运行Test函数会有什么样的结果
💡以上程序共犯如下错误:
GetMemory函数内部创建的数组是在栈区上创建的出了函数,结束函数时,p数组的空间就还给操作系统,返回的地址是没有意义的,如果通过返回的地址去访问内存,就是非法访问内存
🥐Ⅲ.试题三
❓请问运行Test函数会有什么样的结果
💡以上程序共犯如下错误:
没有对内存开辟的空间进行
释放
✨为避免这种错误,可以:
对申请的空间进行
释放,并手动对指针置为NULL
🥐Ⅳ.试题四
❓请问运行Test函数会有什么样的结果
💡以上程序共犯如下错误:
内存被提前释放,造成
非法访问内存的情况
✨为避免这种错误,可以:
释放完内存后,对指针认为置为
NULL,这样就访问不到了
🍞Ⅳ.内存布局
![]()
1. 栈区(stack):
在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。
栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
栈区主要存放运行函数而分配的
局部变量、函数参数、返回数据、返回地址等。2. 堆区(heap):
一般由程序员分配释放; 若程序员不释放,程序结束时可能由 OS 回收 。
分配方式类似于链表
3. 数据段(静态区)(static):
存放
全局变量、静态数据程序结束后由系统释放
4. 代码段:
存放
函数体(类成员函数和全局函数)的二进制代码
🍞Ⅴ.柔性数组
🥐Ⅰ.柔性数组的概念
C99 中,结构中的
最后一个元素允许是未知大小的数组,这就叫做柔性数组成员
💡可表示为:
1️⃣
2️⃣
🥐Ⅱ.柔性数组的特点
❗特别注意:
1️⃣结构中的
柔性数组成员前面必须至少一个其他成员2️⃣
sizeof返回的这种结构大小不包括柔性数组的内存大小3️⃣包含
柔性数组成员的结构用malloc函数进行内存的动态分配,并且分配的内存应该大于结构体的大小,以适应柔性数组的预期大小
👉Eg:
1️⃣
2️⃣
💡由上述的两个例子我们得出:
除
柔性数组成员外的其余成员变量,都遵循着结构体的内存规则:内存对齐即使给了
柔性数组成员空间大小,也依然不会计算其空间
🥐Ⅲ.柔性数组的使用
❗由上述我们可得知:
malloc所开辟的空间是专门为柔性数组成员申请的,其中:1️⃣
malloc里的+号前面的是给其余成员变量开辟空间的【遵循内存对齐,所以为了避免计算直接用sizeof】2️⃣
+号后面的是给柔性数组成员开辟的空间【不用遵循内存对齐,所以可以直接按需开辟】
✨综上: 就给柔性数组成员开辟了100个整型元素的连续空间,这也是为什么柔性数组使得结构体更加灵活啦~
🥐 Ⅳ.柔性数组的优势
👉同学们可以比较以下两组代码,看看有什么不同:
1️⃣
柔性数组![]()
2️⃣
指针模拟柔性数组![]()
❗我们不难发现: 上述 代码1和代码2可以完成同样的功能,但是方法1的实现有两个好处:
1️⃣方便内存释放:
代码1可直接由一个指针释放整个结构体但
代码2需要释放两次:一个是释放结构体,另外一个释放成员变量指针【且需要按顺序释放,先释放
内部,再释放外部】2️⃣这样有利于访问速度:
连续的内存有益于提高访问速度,也有益于减少内存碎片
🥯Ⅴ.总结
✨综上:就是柔性数组的内容啦~
➡️相信大家对动态内存开辟有不一样的看法了吧🧡
🫓总结
综上,我们基本了解了 C 语言中的“动态内存管理”:lollipop:的知识啦~~
恭喜你的内功又双叒叕得到了提高!!!
感谢你们的阅读:satisfied:
后续还会继续更新:heartbeat:,欢迎持续关注:pushpin:哟~
:dizzy:如果有错误❌,欢迎指正呀:dizzy:
:sparkles:如果觉得收获满满,可以点点赞👍支持一下哟~:sparkles:
版权声明: 本文为 InfoQ 作者【Dream-Y.ocean】的原创文章。
原文链接:【http://xie.infoq.cn/article/df5f703182b2940210123198b】。未经作者许可,禁止转载。










评论