文章目录
前言
路漫漫其修远兮,吾将上下而求索。
一、为什么存在动态内存分配
回顾以往使用内存空间的时候,我们既可以直接创建一个变量来申请一小块内存空间,同样也可以申请一大块内存空间(即数组的创建),但是这两种向内存申请空间的形式存在两缺点:
- 开辟的空间大小是固定的,无法改变。
- 创建数组时必须得指定其长度,数组所需要得空间会在编译期间进行分配;
联想到学习中写的通讯录,如果可以动态开辟空间的话,当已经开辟的空间不够的时候再开辟一块空间,不够用了再开辟……而不是一次性开辟一个足够大的空间来存放数据,这种方法不仅难以保证开辟的空间是否够用而且还极有可能浪费许多内存空间,无法做到“因地制宜”;故而动态内存分配的存在是合理的;
接下来我们便来了解一下动态内存;
二、动态内存函数的介绍
1、malloc
- 功能:开辟内存空间
- 所要引用的头文件 : <stdlib.h>
- void* malloc ( size_t size);
- 参数size 为所要开辟空间的大小,单位为字节;
- 如若参数 size 为0,malloc 会怎么去执行这是未被标准定义的,取决于编译器;
- 返回类型为 void* ,故而使用者要根据所开辟空间的用于放置什么样类型的数据来进行强制类型转换;
- 返回值:malloc 开辟空间成功时便会返回此空间的起始地址;倘若开辟空间失败便会返回NULL;故而,在使用 malloc 时还得判断它是否开辟空间成功;
malloc 的使用如下图,此处我使用malloc 的目的是想创建一块能存放10个整型的空间:
详细分析使用:
- 注:你可能会有疑问,在main 函数中,结尾是 return 0; 而此处发生异常时(malloc 开辟空间失败)为什么 return 1?
C语言中的习惯,当main函数 return 0 时以表示正常返回,而如若出现了问题便 return 1;
看到此处可能你会想说利用数组也可以完成以上操作,那么用数组与利用malloc 向内存申请空间,这二者有何区别呢?
二者存在两点不同;
一是在内存中申请的空间位置不同,数组是相同类型的集合,本质上就是变量创建所开辟的空间,即在栈区上申请空间,而malloc 开辟的空间位于堆区;
二是开辟后的空间的可改性,数组在栈区上申请的空间不可被修改,而malloc 在堆区上开辟的空间可以被修改;
当你看到数组的所占用的内存空间不可修改时,是不是会想到C99标准中的变长数组?
敲重点!变长数组的意思并不是说这个数组的大小是可变的,而是说可以根据一个变量来确定一个数组的元素个数,而这个变量一旦确定下来后,数组元素的个数也就确定了;即变长数组可以动态输入一个数值来确定其数组元素的个数;
在上述代码中,我们利用malloc 向内存申请了空间,但是并没有free,便存在内存泄漏;
什么是内存泄漏?
- 并不是表面意思内存不在了,其真正的意思为,当你利用malloc 向操作系统申请了一块内存空间,使用完之后你不用了,但是又没有释放即还给操作系统,那么别人也没有使用这块空间的权限,因为你占着“茅坑不拉💩”,于是乎别人想用也不行;
既然没有归还那为什么内存的空间没有丢失?
- 因为,操作系统已经“进化”得很聪明了,他预判了用户在写代码得时候可能会忘记释放空间,于是乎当程序退出而你没有释放空间的时候,操作系统会自动收回该空间;
如何释放内存空间呢?
C语言提供了一个专门用来释放和回收动态开辟内存函数:free;
2、free
- 功能:释放和回收动态开辟的内存
- 所要引用的头文件 : <stdlib.h>
- void free ( void * ptr);
- 参数ptr 的类型为 void* ,意为动态开辟空间的起始地址;
- 返回类型 为 void ,即无返回值;
- 释放完该空间,要即使将指向此空间的指针置为NULL;因为此指针为野指针,若想要避免野指针就要在释放空间之后即使置空;
将上面 举例的malloc代码完善:
注:当使用了动态开辟内存空间的函数时,不用的时候就一定要利用free 将此空间释放了;
3、calloc
- 功能:动态开辟内存空间并且将该空间中的内容初始化为0
- 包含的头文件: <stdlib.h>
- void* calloc ( size_t num, size_t size);
- 第一个参数 num的类型为size_t ,表示的是动态开辟空间中元素的个数;第二个参数size 的类型为 size_t ,表示一个元素的大小;
- 返回类型为 void *
- 当使用calloc 成功开辟空间的时候,便会返回该空间的起始地址;倘若失败则会返回NULL;
使用calloc 动态开辟能存放10个整型的空间:
注: malloc 仅在堆区开启了一块空间;而calloc 在堆区上开辟空间的基础上还对此空间中的数据初始化为了0;
相当于 calloc 包含了 malloc 的功能;也可以这样理解 calloc = malloc + memset;
realloc(NULL, 40) ; //等价于 malloc(40);
你会不会有疑问,不是说动态开辟内存吗,但是上面的 malloc 和 calloc 也仅仅体现为开辟空间,”动态“体现在何处呢?请接着往下看,realloc 这个函数 与 malloc 和 calloc 的配合使用就能体现"动态内存开辟"喔~
4、realloc
- 功能:重新动态开辟内存空间
- 引用的头文件 : <stdlib.h>
- viod * realloc (void * ptr , size_t size);
- 第一个参数为 ptr,其类型为 void* 代表着在堆区上动态开辟空间的起始地址 ; 第二个参数为 size 其类型为 size_t 代表着重新开辟空间的大小(新空间的总大小),单位为字节;
- 返回类型为 void*
- 返回值:如若利用 realloc 重新开辟空间成功,则会返回该空间的起始地址;倘若失败则会返回NULL;
- realloc 也可能会开辟空间失败,故而也要进行排空的判断;
在calloc 开辟了 40 byte 空间的基础上,再开辟40 byte 大小的空间:
代码如下:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main()
{
//开辟空间
int* pf = (int*)calloc(10, 4);
if (pf == NULL)
{
perror("calloc");
return 1;
}
//使用
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", *(pf + i));
}
printf("\n");
//空间不够,我要再开辟 40byte 的空间,所以新空间的大小为 80 byte
int* ptr = (int*)realloc(pf, 80);
//realloc 也有可能会重新开辟空间失败
if (ptr == NULL)
{
perror("realloc");
return 1;
}
//再使用
pf = ptr;
memset(pf, 0, 80);
for (i = 0; i < 20; i++)
{
printf("%d ", *(pf + i));
}
//释放空间
free(pf);
pf = NULL;
return 0;
}
代码运行结果如下:
- 为何在上述代码中,realloc 的返回值要新创建一个指针 ptr 来接收?
如若用指向上例中 calloc 动态开辟的空间的指针pf 来接收realloc的返回是非常危险的;
因为realloc 也有可能重新开辟空间而返回NULL,倘若realloc 返回NULL 并且用pf 来接收,那么便会找不到 calloc 开辟的空间(因为pf 指向calloc 所动态开辟的空间),就好比想让realloc 对旧空间进行“扩容”处理,没想到不仅realloc 扩容失败了,还将指向旧空间的指针pf 搞成了空指针,从而找不到此旧空间;
故而此处得创建一个新指针来接收 realloc 得返回值来避免“得不偿失”;
小技巧分享:在利用malloc、calloc 、realloc 开辟空间时其参数是所开辟空间的字节大小,直接写数字的字节数容易出错,那么就可以利用 sizeof(类型)*个数 的形式,例如 利用malloc 开辟能存放10个整型的空间便可以写作 int* pf = (int*) malloc (sizeof(int) * 10) ; 倘若想再利用realloc 扩容10个整型的空间 : int* ptr = (int* ) realloc ( pf , sizeof(int) * 20) ;
此小技巧会强行给自己增加一个思考的流程,强烈推荐;(PS:本人在注意力不太集中的情况下极易将上例中 realloc 再次开辟的代码写作 : int * ptr = (int *) realloc ( pf , 40) ; 实际上其第二个参数应该是 80);
realloc 的工作原理:
分为两种情况,一是旧空间后面的空间够用以开辟新空间,二是旧空间后面的空间不够用;
情况一:
堆区中的空间一定也会存有数据故而有些空间占用,当所要重新开辟空间后面的空间不够时,realloc 便会在堆区上找哪块空间是空的并且足以放下新空间大小的内存空间,找到之后,会将"旧空间"中的数据拷贝放入新空间之中,然后再返回此空间的起始地址;(其中旧空间中的数据会自动释放)
情况二:
所要重新开辟的空间后面放得下所要增加的空间,于是就直接在此空间的原位上进行重新开辟空间的操作;
分别调试来看此两种情况:
情况一:
重新开辟400 byte 大小空间的时候,旧空间后面的空间大小不够,于是realloc 便会在堆区中重新找空间来重新开辟空间,于是 ptr 与 pf 中存放的地址不同;
情况二:
旧空间后面的空间足够用,于是就在旧空间的处重新开辟,故而 pf 与 ptr 中存放的地址相同;
总之,realloc 在使用的时候会出现以上两种情况,一是返回的地址为新地址,即此时在旧空间后面存放不下所要增加的空间,于是 realloc 在堆区上找了一块足够大的空间,先将旧空间中的数据拷贝放入新空间中,然后自动将旧空间释放,最后返回新空间的地址,二是返回的地址就是旧地址,此时旧空间后面有足够的空间可以放下增加的空间;
特别注意:如若频繁地使用 malloc、calloc、realloc 来开辟内存空间,便会使得内存空间的使用率下降以及效率下降;
- 因为 malloc 、calloc 、realloc 是在堆区上动态申请的内存空间,而堆区中的内存空间是由操作系统管理的,故而当我们写下malloc、calloc、realloc 来向内存申请空间的时候,会调用操作系用提供的接口然后到堆区上申请空间,每一次向内存申请空间均会打断操作系统的正常执行,因为操作系统要帮助我们在堆区上申请空间,然后我们才得以使用此块空间;
- 显然,让操作系统来申请空间是会花费时间的,故而倘若你频繁地利用 malloc、calloc、realloc 来申请空间,频繁地打断操作系统从而导致操作系统的效率低下,并且会浪费大量的时间;
- 再者,会导致该内存中的内存碎片较多,便会导致空间的利用率下降;
在平时写代码中,倘若难以避免多次使用 malloc、calloc、realloc,而产生较多的内存碎片,那么针对较多的内存碎片,该怎么办呢?
在程序设计中,软件工程里有一个叫内存池的概念;
什么叫作内存池?(了解)
该程序首先会向内存申请一块相对来说可以满足当前程序所需的一块区域,而由于此块区域已分配给当前的程序,程序内部自己便会以内存池的方式来维护此空间;
当该程序想要使用一块空间,便会使用该内存池中的空间,不过用完之后便要将内存归还给内存池;正是程序自己在维护此块内存区域,于是将这块区域称为内存池;
内存池可以解决内存碎片,空间使用效率低下的问题;
三、常见的动态内存错误
1、对NULL指针的解引用操作
简单来说,就是在利用 malloc、 calloc 、 realloc 函数动态开辟内存空间的时候没有去判断他们有没有开辟空间成功,而直接去使用这些函数的返回值;
错误代码如下:
修改后正确的代码如下:
2、对动态开辟空间的越界访问
只用向内存申请了的空间该程序才有使用该空间的权限,该程序去访问、使用不属于该程序的空间便就是非法访问;
同理,对动态开辟所空间进行越界访问便也是非法访问;
3、对非动态开辟的空间使用 free 来释放
free 释放的空间是位于堆区上动态开辟的空间,即利用函数 malloc、calloc、realloc 所开辟的空间;倘若对非动态开辟的空间而利用free 释放是存在问题的;
4、使用 free 释放一块动态开辟空间的一部分
(释放动态内存开辟的空间,没有完全释放)
例子如下:
此时 pf 已经不再指向 malloc 所动态开辟空间的起始地址,free 无法释放动态开辟空间的一部分(硬性规定);
free 所释放的空间必须从动态所开辟空间的起始地址开始;
故而用于接收 malloc 所开辟空间的返回值的指针 pf 在使用的过程中不可被修改,准确来说得保证有个指针指向这块动态开辟得空间,以确保free 可以次空间释放;
- 你可能会想到能不能用const 来修饰指针pf ,即效果如下:
显然也是不可行得,因为利用free 将空间释放之后,为避免野指针得将指向这块空间得指针及时置空,但是倘若用 const 来修饰 指针pf 便无法执行操作 pf=NULL;
5、对同一块动态内存的多次释放
错误代码如下:
针对此错误有两种解决方案:
方案一:确保动态开辟的空间只利用 free 释放一次
方案二:在使用free 释放空间之后及时置空
方案一:
方案二:
- 为何 free(NULL); 不会产生太大的影响?
因为NULL 为空指针,空指针代表着没有明确的指向空间,而free 是将某个动态开辟的空间所释放;free (NULL); free 的参数为 NULL,便就意味着free 不知道释放哪块空间--> 不用释放空间;故而不会产生太大的影响;
6、动态开辟的空间忘记释放而造成内存泄漏
案例一:在函数中跳过了释放空间的代码
分析:在 test 函数中利用malloc 动态开辟的空间只能在 test 函数中被释放,因为用来接收malloc 返回值的指针pf是个局部变量,出了其作用域那么pf 便会被销毁即pf 所占用的内存空间会还给操作系统;故而倘若在test 函数之中未能将 pf 所指向的空间给释放了那么在后面便也没有可能能释放该空间;
案例二:使用封装的专门动态开辟内存空间的函数之后忘记释放空间
四、C\C++ 程序的内存开辟
内存分为内核空间、栈、内存映射段、堆、数据段、代码段;
- 内核空间是用来跑操作系统内核的一块空间,故而用户不可以对此空间进行访问;
- 栈区(Stack):用来栈区主要存放运行函数而分配的局部变量、函数参数、返回地址等的空间,在使用栈区中的空间时默认先使用高地址处的空间(与环境有关);在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放-->函数栈帧的创建与销毁。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
- 堆区(Heap):前面学的利用malloc calloc realloc 动态开辟所申请的空间;即堆区中的空间一般是由程序员分配并释放,如若程序员不释放,程序结束的时候可能由OS回收,分配类似于链表;
- 数据段是我们之前接触的静态区,是用来存放静态变量以及全局变量的空间,程序结束后由系统释放;
- 代码段:存放编译之后形成可执行程序的二进制指令的空间;
内存中的布局分布如下图所示:
注:看到这,你可能就会对之前的知识:当局部变量被static 修饰时,其生命周期会变长 理解更加深刻了;
- 因为局部变量存放在栈区,而栈区上的空间会随着函数的执行而创建或者销毁,也就是说当出了此局部变量的作用域,该局部变量便会销毁;而静态变量是存放在数据段的,数据段中的数据存在于整个程序的运行期间 ,并且在程序结束后才会由系统释放;故而当局部变量被static 修饰的时候,其生命周期会变长;
五、柔性数组
在C99标准中,结构中的最后一个元素允许为未知大小的数组,这便叫做柔性数组的成员;柔性数组在结构体变量中有且只有一个;
写法:
注:这两种写法选其一便可,在一些编译器上倘若使用写法一会报错那便使用写法二;
1、柔性数组的特点
- 结构体中的柔性数组成员前面必须至少有一个其他成员;
- sizeof 返回的这种结构的大小不包括柔性数组所占的内存;
- 包含柔性数组成员的结构体需要用malloc 对其进行动态内存的分配,并且所分配的内存应该大于结构体的大小以适应柔性数组的预期大小;
具体分析:
结构体中柔性数组成员的前面必须至少有一个其他成员;因为柔性数组所占用的内存空间的大小是不确定的,故而sizeof 返回的这种包含柔性数组的结构的大小时不会计算柔性数组的大小;
显然,"柔性"体现在可曲可伸,那么其空间的开辟必然会利用到动态内存开辟的相关知识;
2、柔性数组的使用
柔性数组作为结构体的成员,能否如同普通的结构体变量一样创建呢?
如下图所示:
显然这种写法是不可以的,因为sizeof(struct S) 的结果为4byte ,那么struct S s = {0};创建的结构体变量s 便只会向栈区申请 4byte 的内存空间,4 byte 大小的空间只能放下成员 i的数据,而没有给 数组a 任何的空间;故而此种写法不可行;
如何做?需要利用 malloc 进行动态内存的开辟;
代码如下:
#include<stdio.h>
#include<stdlib.h>
struct S
{
int i;
int a[];
};
int main()
{
//struct S s ={0 }; 这是错误的写法
struct S* pf = (struct S*)malloc(sizeof(struct S) + sizeof(int) * 10);
if (pf == NULL)
{
perror("malloc");
return EXIT_FAILURE;
}
//使用
pf->i = 4;
int j = 0;
for (j = 0; j < 10; j++)
{
(pf->a)[j] = j;
}
for (j = 0; j < 10; j++)
{
printf("%d ", (pf->a)[j]);
}
//释放空间
free(pf);
pf = NULL;
return 0;
}
代码运行结果如下:
图解如下:
倘若利用malloc 开辟的这10 个整型的空间不够用,还可以利用realloc 对内存进行调整;
看到此处你可能会在想:只要知道数组的起始地址,其元素类型以及元素个数,我们便可以访问数组所占用的这块空间;那么在结构体中,我们将数组的起始地址作为一个成员,再将malloc 动态开辟的空间的地址传给结构体中保存数组起始地址的指针当中;和前面的代码一样,让整个结构体在堆区上申请空间来存放数据,所以此处会用到两次malloc ,一次用来为整个结构体申请空间,一次是为数组申请空间;
图解如下:
代码如下:
#include<stdio.h>
#include<stdlib.h>
struct S
{
int i;
int* a;
};
int main()
{
//为结构体开辟空间
struct S* ps = (struct S*)malloc(sizeof(struct S));
if (ps == NULL)
{
perror("struct S malloc");
return 1;
}
//为数组开辟空间
int* pf = (int*)malloc(sizeof(int) * 10);
if (pf == NULL)
{
perror("a malloc");
return 1;
}
//使用……
//释放
free(pf);
free(ps);
ps = NULL;
return 0;
}
这两种方式哪方式好?
利用柔性数组的好,即第一种方式好;存在两个原因:
- 1、第二种方法malloc 的次数比第一种方法malloc 的次数多;malloc 的次数越多,那么free的次数也会越多,那么忘记释放空间的可能性便会越大,即内存泄漏的可能性越大且更容易出错;
- 2、如若在内存中频繁地进行malloc ,malloc 地次数越多,内存中产生内存碎片地可能性便会越大;在前面地学习中,我们已知内存碎片越多那么该程序内存地利用率与使用效率会下降;
故而,方法一优于方法二;
3、柔性数组的优势
1、方便内存释放
- 如果我们的代码是在一个给人使用的函数中,你在里面做了两次内存分配,并把整个结构体返回给用户,用户调用free 便可以释放空间,但是用户并不知道你在这个结构体成员中又进行了一次内存开辟,那么用户便不会释放该空间,因为用户根本不知道;所以,如果我们要把结构体的内存以及其成员的内存一次性分配好,那么用户也仅仅需要free 一次空间即可;
2、有利于访问速度
- 应用连续的内存来存放数据有利于提高该数据的访问速度;
总结
1、malloc calloc realloc free 的使用
- void* malloc(size_t size);
- void* calloc(size_t num , size_t size);
- void* realloc( void* ptr , size_t size);
- void free( void * ptr);
2、malloc calloc realloc 均有可能开辟空间失败,故而在使用之前要进行判空
3、释放动态开辟的空间之后置空,可以避免很多不必要的麻烦
4、内存分为内核空间、栈区、内存映射段、堆区、数据段、代码段
5、柔性数组存在于结构体中,并且在此结构体中有且只有一个柔性数组;在柔性数组成员之前至少有一个其他类型的成员;利用sizeof 计算该结构体的大小的时候,并不会计算柔性数组成员的大小;
6、柔性数组成员所占空间的大小需利用 malloc、calloc 来开辟,可以利用realloc 来调整其大小;