0. 前言
在平常开辟数组的时候,你是否为空间不足、空间浪费、空间无法调整而烦恼?如果对此头疼不已,相信看完这篇博客,你的问题就能迎刃而解。没错,本篇博客就是对动态内存管理的讲解。博客中,对于动态内存的相关函数、使用动态内存时经常出现的问题,和几道经典笔试题做了详细讲解。话不多说,我们这就开始。
1. 为什么存在动态内存分配
我们已经掌握的内存开辟方式有:
int val = 20;//在栈空间上开辟4个字节 char arr[20] = { 0 };//在栈空间上开辟20个字节的连续空间
但是上述的开辟空间的方式有两个缺点:
空间开辟大小是固定的,无法扩容或减容,可能会空间不足或空间浪费。
数组在定义的时候,必须指定数组的长度,它所需要的内存在编译时分配。
但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足了(数组需要提前指定好大小,因为编译的时候需要确定函数栈空间大小,遇到运行位置才能确定大小的情况就不太适合了),那么这时不如试试动态内存开辟!
2. 动态内存函数
2.1 malloc
c语言提供了一个动态内存开辟的函数:
void* malloc (size_t size);
malloc是一个开辟动态内存的函数,参数size
为开辟空间的内存大小,单位是字节。函数返回值为void*
的指针,开辟成功返回开辟空间的地址,失败返回NULL空指针。
2.1.1 申请空间成功
例如,开辟一个四十个字节的空间:
#include <stdlib.h>//所需头文件 int main() { void* p = malloc(40); return 0; }
但是这样使用还是不够准确的,因为p的类型是void*
。void*
的指针也不知道步长,也不能解引用,也不能±,不如我们直接将p强制类型转换成对应类型。就比如我们想申请一个10个整形元素的空间。
int* p = (int*)malloc(40);
当malloc成功申请到空间,返回这块空间的起始地址。
2.1.2 申请空间失败
倘若我们申请空间,失败了。例如我内存只有4个G,但是我要申请1个T的空间,这时就会返回NULL空指针。
所以当空间开辟失败这是很危险的,所以在每次开辟空间后最好来一个判断:
int main() { int* p = (int*)malloc(INT_MAX);//21亿多,整形的最大值 if (p == NULL) { printf("%s\n", strerror(errno));//打印错误码,了解错误 return 1;//异常返回 }//使用 return 0; }
运行结果:
这里也可以用断言,断言为直接将程序奔溃,雷厉风行;而if语句则是一个委婉的处理,让我们看到对应的错误。一般在传参时参数检查使用断言,malloc等开辟空间的函数使用if语句判断是否为空指针。
int main() { int* p = (int*)malloc(INT_MAX);//21亿多 assert(p);//断言 return 0; }
运行结果:
2.1.3 总结
void* malloc (size_t size);
这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
size是需要动态开辟空间的内存大小。
如果开辟成功,则返回一个指向开辟空间起始处的指针。
如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
返回值的类型是void*,所以malloc函数并不知道开辟空间的类型,具体在使用的时候自己来决定。
如果参数size为0,malloc的行为是标准未定义的,取决于编译器。
2.2 free
2.2.1 free的重要性
我们平常创建的局部变量,出作用域自动销毁,它在栈区
上开辟。
而动态内存分配,如malloc等函数是在堆区
上开辟空间的,它并不会自动销毁,需要自己回收,或者程序结束时自动回收。
但是程序结束时自动回收有个缺点,当这个程序不结束时,这块空间就会一直存在。试想一下,如果运行一个大规模的程序,程序运行的周期很长,但是动态内存一直在开辟空间,也不释放,最后会不会因为内存不足,导致内存耗干?导致电脑卡死?
然后就会出现某些灵异现象,程序一跑起来就很卡,过一会程序结束就没了,或者没有关掉程序,然后电脑越来越卡,只能重启,重启完毕又好了的事情,如果内存没有及时释放,你说多恐怖?这件就是典型的"吃内存"现象。
所以C语言提供了另外一个参数free,专门用来做动态内存的释放和回收:
void free (void* ptr);
free用来释放动态内存开辟的空间,参数ptr为指向开辟空间的首地址处的指针。函数没有返回值。若参数为动态开辟的起始地址,释放空间。若参数为NULL空指针,则不进行操作。
2.2.2 free的使用
例如:
int main() { int* p = (int*)malloc(40); if (p == NULL) { printf("%s\n", strerror(errno));//打印错误码,了解错误 return 1;//异常返回 } //使用 int i = 0; for (i = 0; i < 10; i++) { *p = i; p++;//改变指向,p最后指向空间的结尾后面位置 } //释放 free(p);//ok? return 0; }
这样做可不可行?答案是不行的,因为使用动态开辟的空间时,p被修改了,这时p释放的不是我们开辟的空间,这样就出问题了。
我们应该额外保存一份p的拷贝,用拷贝进行使用,最后再释放p的空间:
int main() { int* p = (int*)malloc(40); int* ptr = p; if (p == NULL) { printf("%s\n", strerror(errno));//打印错误码,了解错误 return 1;//异常返回 } //使用 int i = 0; for (i = 0; i < 10; i++) { *ptr = i; ptr++;//改变指向,p最后指向空间的结尾后面位置 } //释放 free(p); p = NULL;//及时置空 ptr = NULL;//最好这样,防止把ptr误用 return 0; }
注意:
p需要及时置为空指针。当对p进行释放时,p对应的空间被置为随机值
,但是p本身的地址还没有改变。这是很麻烦的,万一有人不知道,又使用了之前开辟的空间,这块空间我们已经还给操作系统无法使用了,这时访问了就属于非法访问,所以要及时置为空指针,让它无法被访问。
设想一下,如果我们把p释放了,但是没有置空,那它是不是个野指针,对应着指针的指向被释放。野指针很危险,现在拿NULL把它限制住了,我们不就安全了?
但是这里最好把ptr也置为空指针,因为ptr当前指向了不属于我们当前程序的空间,为防止误用,还是置空。但是ptr不用free,因为ptr指向的空间不是我们动态开辟的。
2.2.3 总结
void* free (void* ptr);
ptr是值为动态内存开辟的起始地址的指针。
free释放的是动态开辟的指针ptr指向的空间,而不是ptr本身,指针需指向开辟空间的首地址处。
如果ptr指向的空间不是动态开辟的,那free函数的行为是未定义的。
如果ptr是NULL空指针,则函数什么事都不做。
free释放空间后,需要将ptr置为空指针,防止野指针问题(指针指向空间被释放),造成非法访问。
2.3 calloc
倘若我们已经有了明确的目的我们要开辟多大的空间,类型是什么。那么我们就可以使用calloc函数。
calloc和malloc一样,也是由C语言提供,用来动态内存分配:
void* calloc (size_t num, size_t size);
calloc也是动态内存开辟空间的一个函数,参数num
为开辟空间的元素个数,参数size
为开辟空间元素的大小,单位是字节。函数返回值为void*
,开辟成功返回开辟空间的地址,失败返回NULL空指针。
2.3.1 calloc的使用
和malloc一样,calloc返回值也是void*
,所以我们在使用时需要强制类型转换。
例如开辟一个40字节,用来存储整形的空间:
int main() { int* p = (int*)calloc(10, sizeof(int)); if (p == NULL) { perror("calloc");//打印错误信息 return 1; } int i = 0; //使用 for (i = 0; i < 10; i++) { *(p + i) = i;//不改变指向 } //释放 free(p); p = NULL; return 0; }
calloc开辟空间失败也会返回NULL,所以需要判断。并且需要释放开辟的空间,这里由于p并没有改变指向,p还是指向原来的位置,所以直接释放p置空即可。
2.3.2 malloc和calloc的区别
- malloc传参时直接传递开辟空间的大小,calloc传参时传元素个数和元素的大小。
- malloc开辟的空间默认值为随机值,calloc开辟的空间默认值为0。
calloc相当于把开辟的空间每个元素设置为0,然后返回起始地址。相当于calloc = malloc + memset(内存设置为0)。
总结:
开辟的空间需要初始化,使用calloc,不需要初始化,使用malloc。但是malloc不初始化效率会更高,calloc效率较malloc会比较低。
2.3.3 总结
void* calloc (size_t num, size_t size);
num
是开辟空间的元素个数,size
为开辟空间元素的大小。- 函数的功能是开辟
num
个大小为size
的空间,并且把空间的每个字节初始化为0. - 与函数malloc的区别在于calloc会在返回地址之前把申请空间的每个字节初始化为全0。
2.4 realloc
realloc函数的出现会让动态内存管理更加灵活。
有时我们发现申请的空间太小了,有时我们又会觉得申请的空间过大了,那为了合理的申请内存,我们就必须对内存大小做出灵活的调整,那么realloc
函数就可以做到对动态内存开辟内存大小的调整。
void* realloc (void* ptr, size_t size);
realloc是调整动态开辟内存大小的函数,ptr
为指向动态内存开辟空间的指针,size
为调整过后这块空间的大小,单位是字节。函数返回值为void*
,调整成功返回指向调整之后的内存块,失败返回NULL空指针。
2.4.1 realloc调整空间的两种情况
- 当前内存空间大小充足,则跟着原先开辟的空间继续向后开辟,返回原来的空间的起始地址。
当前内存空间大小不够,重新寻找内存,单独开辟一块全新的空间,空间大小满足调整大小。将原先空间的数据先拷贝到当前空间,再释放掉原先的空间,返回新开辟空间的起始地址。
realloc调整后的空间比原先空间小,直接在原先空间的基础上缩短空间大小,返回原来空间的起始地址。
2.4.2 realloc的使用
对于realloc调整内存,还是要着重强调一下前两种情况:
- 内存足够在原有内存之后追加空间,返回原先空间的起始地址。
- 内存不足重新开辟调整大小的空间,先拷贝数据,在释放原先空间,返回新空间起始地址。
例如,一个realloc的正常使用:
int main() { //动态开辟 int* p = (int*)malloc(40); if (p == NULL) { return 1; } //使用 int i = 0; for (i = 0; i < 10; i++) { *(p + i) = i; } //打印 for (i = 0; i < 10; i++) { printf("%d ", *(p + i)); } //增加空间 int* ptr = (int*)realloc(p, 80); //判断 if (ptr != NULL) { p = ptr; ptr = NULL;//防止ptr误使用 } //扩容后使用 for (i = 10; i < 20; i++) { *(p + i) = i; } //释放 free(p); p = NULL; return 0; }
这里有几个注意点,需要重点提一下。
2.4.2.1 注意点 1
一定要接收realloc的返回值。
首先,得了解函数调整内存的情况。不要不知所云就认为realloc
不管什么情况都是以原先空间的基础上向后延伸.
一定要返回值接收,否则当开辟空间足够大,返回新空间的地址时,如果我们不用返回值接收,就像这样:
int main() { int* p = (int*)malloc(8000); if (p == NULL) { return 1; } //使用 int i = 0; for (i = 0; i < 10; i++) { *(p + i) = i; } realloc(p, 80);//无返回值接收 //扩容后使用 for (i = 10; i < 20; i++) { *(p + i) = i; } free(p); p = NULL; return 0; }
运行一下:
分析:
程序直接奔溃了,因为realloc调整空间时,发现空间不足,只能找一块全新的位置开辟,将原先的空间释放掉了,而我们并没有用返回值接收调整p,那么就用p非法访问了内存。
2.4.2.2 注意点 2
用全新的指针接收realloc的返回值,而不是直接用动态开辟内存的指针接收。
我们知道realloc调整空间失败返回NULL空指针。
如果将NULL赋给原先指向开辟空间的p指针。比如,p原本指向40个字节的空间,但是空间调整失败了,直接给我弄成了空指针。这不是偷鸡不成蚀把米嘛!连原本的空间都没了,你说realloc这个老六干的什么事情!
所以我们需要用一个全新的指针来接收,比如这样:
int* ptr = (int*)realloc(p, 80);
当然仅仅用返回值接收肯定不够,当然还要赋给我们之前的指针。当然在这时要对返回值做出判断,并且及时将ptr置空。因为ptr被赋值,以后这块空间就由先开始的指针进行管理并释放,为了保险起见,不让ptr影响p的操作,于是把ptr置空,防止误操作。
int main() { int* p = (int*)malloc(40); if (p == NULL) { return 1; } int* ptr = (int*)realloc(p, 80); if (ptr == NULL)//判断 { p = ptr; ptr = NULL;//置空 } return 0; }
2.4.2.3 注意点 3
当第一个参数为NULL空指针时,realloc起到和malloc/calloc一样的作用。
int main() { int* p = (int*)realloc(NULL, 40);//等价于malloc(40) return 0; }
2.4.3 总结
void* realloc (void* ptr, size_t size);
ptr 是要调整的内存地址
size 是调整之后新大小
返回值为调整之后的内存起始位置。
这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
realloc在调整内存空间的存在主要两种情况:
情况1:原有空间之后有足够大的空间。
情况2:原有空间之后没有足够大的空间。
一定要接收realloc的返回值。
用全新的指针接收realloc的返回值,而不是直接用动态开辟的指针接收。
当ptr为NULL空指针时,realloc起到和malloc/calloc一样的作用。
2.5 malloc/calloc和free的问题
malloc和free的次数相同,不能开辟空间不释放,会造成内存泄漏。也不能多次释放,同样的对于calloc也是这样。
malloc/calloc不成对出现代码一定错误,但是malloc/calloc成对出现也可能写不出正确的代码。
举个例子:
int test() { int* p = (int*)malloc(40); if (p == NULL) { //... return 1; } //使用 if (1)//某个条件满足 { return 2;//条件满足返回 } //释放 free(p);//没有释放 p = NULL; return 0; } int main() { test(); return 0; }
分析:
这里malloc和free成对出现,但是由于满足条件,函数提前结束了,然后p指向的空间就没有释放,依然错误。
而p又是在函数中创建的,等函数结束,p也销毁,也并没有返回值来记住p,p在函数中指向的那块空间是被开辟的,但是出了函数就没人知道这块空间在哪里,这就造成了内存泄漏。