allocate
v.分配,分派,划拨
1.动态内存
⚀定义
动态内存是指动态内存空间,意思就是能动态开辟的内存空间,动态就是指这段空间可以人为的设置大小,可大可小,大小可不固定。
而我们在前面学习的所有变量的内存空间都是创建后不可改变大小的,比如使用int类型创建变量,创建数组,他们的大小在使用前就已经规定好了,但是!!!有些内存空间大小只有等到我们要使用的时候才能确定大小,这时官方就引入动态内存来达到每个程序员都能自己设置内存大小的目的。
⚁内存分区
前面我们学习的变量有全局变量,静态变量(static修饰),局部变量,函数形参,const修饰的变量,
♾栈区:
Stack
存放:局部变量、const修饰的局部变量、函数形参和函数返回值。
管理:由编译器自动分配,操作系统自动管理。
开辟:空间连续,满足从高地址到低地址增长;编译期间大概能确定大小(有一定的预测和规划),程序执行过程中函数调用时分配。
释放:满足先进后出的原则,高地址后释放,低地址先释放;数据在函数结束后自动销毁释放。
特点:内存分配运算内置于CPU的指令集,效率高;但是分配空间有限,空间小。
♾堆区:
Heap
存放:动态分配的变量、大型数据、长期存在的数据和指针类型数据等等啦。
管理:由程序员手动分配,手动释放,存在内存泄漏的情况。
开辟:内存空间不连续,常使用malloc函数、calloc函数和realloc函数进行开辟,满足内存从低地址到高地址依次开辟;编译期间不确定大小,运行时确定分配。
释放:常使用free函数进行释放,不释放则一直在,程序结束时收回。
特点:开辟时需查找可用内存块并标记,存在内存碎块降低分配效率,效率低;大小受限计算机中有效的虚拟内存,空间大。
♾静态区:
存放:全局变量和静态变量,该变量在程序的整个生命周期中都存在。
管理:由编译器自动分配,操作系统自动管理。
开辟:空间连续,满足从低地址到高地址增长;在程序编译时确定大小,完成内存分配。
释放:释放空间是一次性全部释放完,不是栈区的先进后出的原则;数据在程序结束后自动释放。
特点:空间连续,效率高,但开辟操作是编译器,而不是CPU中的指令集。
静态分为.data段和.bss段【补充】
.data段:
- 存放程序中已经初始化的全局变量和静态变量。
- 由于这些变量有具体的初始值,因此它们的内容需要被存储在可执行文件中。
- 当程序被加载到内存时,.data段中的数据将被复制到相应的内存地址,并且可以直接使用。
.bss段:
- 存放程序中未初始化的全局变量和静态变量。
- 这些变量在程序加载时由系统自动初始化为零。
- 与.data段不同,.bss段在可执行文件中不占用实际的磁盘空间,它只在符号表中记录符号,并在段表中记录大小。【扩充】
- 这样做可以节省可执行文件的空间,因为不需要为这些变量存储实际的初始值。
♾字符常量区:
存放:字符串、数字和const修饰的全局变量。
管理:由编译器自动分配,操作系统自动管理。
开辟:空间连续,满足从低地址到高地址增长;与静态区类似,编译时确定大小,完成内存分配。
释放:与静态区类似,一次性释放完,数据在程序结束后自动销毁释放。
特点:与静态区类似,内存空间连续,效率高;数据不可以被修改。
♾程序代码区:
存放:存放程序执行代码,#define定义的常量和字符串常量有可能存储在这。
管理:由编译器自动分配,操作系统自动管理。
特点:不能被修改,大小取决于编译后的二级制代码的大小。
下面以为大家介绍动态内存管理的4大函数 :
2.malloc函数
⚀函数作用
Allocate memory block.
malloc函数用于分配内存块,向堆区申请开辟指定size的一段连续的内存空间。
⚁函数定义
void* malloc (size_t size);
- size_t size 分配内存块的大小,单位字节,通俗讲就是开辟size个字节的内存空间。
- void* 返回开辟后空间的地址,但该空间存储数据的类型未知,所以用void*指针返回。
⚂函数使用
int* p1 = (int*)malloc(sizeof(int)*5); char* p2 = (char*)malloc(sizeof(char) * 8);
- 开辟5个int的空间,我们知道要存int的类型的数据,就必须在malloc函数调用完后将void*的指针强转int*,再用int*接收;若不强转直接接收会报错。
- 开辟8个char的空间,同上,必须使用char*强转在接受。
总结:使用malloc函数后必须将void*指针强转成你要使用的指针指针。
⚃注意事项
- size的值为0,具体操作行为取决编译器。
- size的值过大会导致开辟空间失败。
- 开辟成功返回空间的起始地址,开辟失败则返回空地址(空间分散,碎块间隙大小不满足)。
- 函数使用后必须强转,再拿指针接收。
- 开辟后在用接收空间的指针时,一定要判断指针是否为空,开辟是否成功。
3.calloc函数
⚀函数作用
Allocate and zero-initialize array
calloc函数也用于分配内存块,向堆区申请开辟num个大小为size的元素的一段连续的内存空间,并且将空间的每个内存单元都初始化为0。
⚁函数定义
void* calloc (size_t num, size_t size);
- size_t num 要开辟元素空间的个数。
- size_t size 单位元素空间的大小,单位为字节。
- void* 返回开辟后空间的地址,但该空间存储数据的类型未知,所以用void*指针返回。
⚂函数使用
int* p3 = (int*)malloc(5, sizeof(int)); char* p4 = (char*)malloc(8, sizeof(char));
- 开辟5个大小为int的内存空间,事实上calloc跟创建数组类似,数组就是为n个相同类型的元素开辟的空间,而calloc也是为num个元素开辟空间,只不过calloc函数开辟的空间更加灵活,元素大小可规定,空间不够可增加。
- 开辟8个大小为char的内存空间,跟上面的malloc类似,都是开辟空间,malloc只不过把元素大小和个数乘在一起,直接用总大小开辟,而calloc用个数+元素,当我们知道要存什么大小的元素,存几个时,用calloc的可读性更好。
⚃注意事项
- size的值为0,具体操作行为取决编译器。
- size的值过大会导致开辟空间失败。
- 开辟成功返回空间的起始地址,开辟失败则返回空地址(空间分散,碎块间隙大小不满足)。
- 函数使用后必须强转,再拿指针接收。
- 开辟后在用接收空间的指针时,一定要判断指针是否为空,开辟是否成功。
这几条都和malloc一模一样,下面看下不一样的:
- calloc相比于malloc,开辟后会将空间中的内存单元都初始化为0。
4.realloc函数
⚀函数作用
Reallocate memory block.
realloc函数用于重新分配内存块,将ptr所指向的内存空间修改为指定大小。
⚁函数定义
void* realloc (void* ptr, size_t size);
- void* ptr 需要修改的内存空间的起始地址。
- size_t size 重新分配内存块的大小,单位字节。
- void* 返回开辟后空间的地址,但该空间存储数据的类型未知,所以用void*指针返回。
⚂函数使用
int* p1 = (int*)malloc(sizeof(int) * 5); int* p2 = (int*)calloc(6, sizeof(int)); int* p3 = (int*)realloc(p1, sizeof(int) * 12); int* p4 = (int*)realloc(p2, sizeof(int) * 7);
- 这里将动态开辟(malloc、calloc和realloc函数)的内存进行重新分配,重新分配存在两种情况,这里借这个使用给大家详细讲解一下。
- 第一种情况就是原空间后面没有足够的空间来分配,第二种情况就是原空间后面还有足够的空间进行分配。
- 第二种情况相对简单点,就直接在后面开辟,返回原空间的起始地址;而第一种情况,则需要再重新找一块足够的空间,再将原空间的内容拷贝到新的空间里,在返回新空间的起始地址。
根据上面,小编在VS编译器x64环境运行调试:
下面是p1指向的空间进行扩容,但后面空间不够的情况,返回的是新空间的起始地址:
那么为什么会存在空间不够的情况呢?
其实在这个问题在介绍堆区的时候已经讲过了,就是因为堆区的空间不是连续的是发散的,那就意味着都是堆区的空间都是一个个大小不一的内存块组合而成,是有一定大小的内存块就肯定该空间就不一定不满足我们需要的重新分配的空间大小。
⚃注意事项
- size的值为0,具体操作行为取决编译器。
- size的值过大会导致开辟空间失败。
- 开辟成功返回空间的起始地址,开辟失败则返回空地址(空间分散,碎块间隙大小不满足)。
- 函数使用后必须强转,再拿指针接收。
- 重新分配后在用接收空间的指针时,一定要判断指针是否为空,开辟是否成功。
这几条同样也都和malloc一模一样,下面看下不一样的:
- 如果ptr为空指针,那么他就和malloc函数的功能是一样的,开辟一段空间返回起始地址。
- realloc函数重分配由于原空间后面空间有限,存在返回原空间地址和新空间地址两种情况。
5.free函数
⚀函数作用
Deallocate memory block.
free函数用于释放内存块,将ptr指向的内存空间释放掉。
⚁函数定义
void free (void* ptr);
- void* ptr 需要释放的内存空间的起始地址。
- void 函数返回值为空,这只是一个释放动态内存的过程。
⚂函数使用
int* p1 = (char*)malloc(sizeof(int) * 6); char* p2 = (char*)calloc(4, sizeof(char)); char* p3 = (char*)realloc(NULL, sizeof(char) * 3);//传NULL和malloc函数的功能相同 free(p1); free(p2); free(p3);
- 将动态内存开辟的空间(malloc、calloc和realloc函数)进行释放,调用free函数,无返回值无需接收。
- 在调用free函数释放后,其实都要将指针置为空,规避野指针。
⚃注意事项
- free函数释放的是动态内存开辟的空间,其他类型空间不能被使用free释放。
- 释放后一定要将指针置为空
- ptr必须是有效指针,不要是NULL、部分空间的地址和未知空间的地址,释放这些都达不到有效目标。
6.常见的错误
动态内存管理有个很重要的点就是:
开辟的空间需要我们释放,经常会在这部分出问题,下面就为大家总结一些常见的错误。
⚀free函数四大特参
☢︎ptr为NULL☢︎
- 有可能是开辟失败造成的指针为空
- 人为的传入空指针
![]()
对NULL指针的释放毫无意义
☢︎ptr为未开辟空间的地址☢︎
- ptr指针指向未开辟的空间,比如指针自++超出范围。
![]()
指针指向未开辟空间,释放错误
- 优化:使用指针时避免指针偏移,尽量保持初始位置。
☢︎ptr为部分动态内存空间的地址☢︎
- ptr指向部分空间的地址,会造成释放不完全,空间残留的结果。
![]()
指针指向部分空间,释放不完全
- 优化:也是使用指针时避免指针偏移,尽量保持初始位置。
☢︎ptr为非动态开辟的空间指针☢︎
- ptr指向非动态开辟的空间时,如释放int,char创建的局部或全局变量,会出现错误。
![]()
指针指向非动态开辟的空间,释放错误
⚁忘记释放和重复释放
☢︎忘记释放☢︎
- 对于已经没用的动态空间忘记释放,会使得堆区空间不断减小,发生内存泄漏。
![]()
不释放空间
- 优化:对已经没用的空间进行及时释放,避免内存泄漏。
☢︎重复释放☢︎
- 对已经释放过的空间再次释放,就是对未分配的空间进行释放,会出错。
![]()
重复释放空间
- 优化:对于已经释放过的空间,避免再次释放。
当然,讲完释放常用的错误 ,还有小编一直强调在使用开辟函数时的错误:
在使用这些开辟函数后,用指针接收前,必须进行强制类型转换,
虽然函数设计不知道我们会使用什么类型数据,但当自己在使用时我们是知道的,所以必须强转。
在使用前,必须进行判断指针是否为空,事实上就是判断开辟是否失败。
7.例题
▶︎例题1◀︎
🟢题目:
void GetMemory(char *p) { p = (char *)malloc(100); } void Test(void) { char *str = NULL; GetMemory(str); strcpy(str, "hello world"); printf(str); }
🟣分析:
▶︎例题2◀︎
🟢题目:
char *GetMemory(void) { char p[] = "hello world"; return p; } void Test(void) { char *str = NULL; str = GetMemory(); printf(str); }
🟣分析:
▶︎例题3◀︎
🟢题目:
void GetMemory(char** p, int num) { *p = (char*)malloc(num); } void Test(void) { char* str = NULL; GetMemory(&str, 100); strcpy(str, "hello"); printf(str); }
🟣分析:
▶︎例题4◀︎
🟢题目:
void Test(void) { char* str = (char*)malloc(100); strcpy(str, "hello"); free(str); if (str != NULL) { strcpy(str, "world"); printf(str); } }
🟣分析:
本章内容结束,下章见,拜拜!!!