文章目录
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>//所需头文件intmain(){void* p =malloc(40);return0;}
但是这样使用还是不够准确的,因为p的类型是
void*
。
void*
的指针也不知道步长,也不能解引用,也不能±,不如我们直接将p强制类型转换成对应类型。就比如我们想申请一个10个整形元素的空间。
int* p =(int*)malloc(40);
当malloc成功申请到空间,返回这块空间的起始地址。
2.1.2 申请空间失败
倘若我们申请空间,失败了。例如我内存只有4个G,但是我要申请1个T的空间,这时就会返回NULL空指针。
所以当空间开辟失败这是很危险的,所以在每次开辟空间后最好来一个判断:
intmain(){int* p =(int*)malloc(INT_MAX);//21亿多,整形的最大值if(p ==NULL){printf("%s\n",strerror(errno));//打印错误码,了解错误return1;//异常返回}//使用return0;}
运行结果:
这里也可以用断言,断言为直接将程序奔溃,雷厉风行;而if语句则是一个委婉的处理,让我们看到对应的错误。一般在传参时参数检查使用断言,malloc等开辟空间的函数使用if语句判断是否为空指针。
intmain(){int* p =(int*)malloc(INT_MAX);//21亿多assert(p);//断言return0;}
运行结果:
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,专门用来做动态内存的释放和回收:
voidfree(void* ptr);
free用来释放动态内存开辟的空间,参数ptr为指向开辟空间的首地址处的指针。函数没有返回值。若参数为动态开辟的起始地址,释放空间。若参数为NULL空指针,则不进行操作。
2.2.2 free的使用
例如:
intmain(){int* p =(int*)malloc(40);if(p ==NULL){printf("%s\n",strerror(errno));//打印错误码,了解错误return1;//异常返回}//使用int i =0;for(i =0; i <10; i++){*p = i;
p++;//改变指向,p最后指向空间的结尾后面位置}//释放free(p);//ok?return0;}
这样做可不可行?答案是不行的,因为使用动态开辟的空间时,p被修改了,这时p释放的不是我们开辟的空间,这样就出问题了。
我们应该额外保存一份p的拷贝,用拷贝进行使用,最后再释放p的空间:
intmain(){int* p =(int*)malloc(40);int* ptr = p;if(p ==NULL){printf("%s\n",strerror(errno));//打印错误码,了解错误return1;//异常返回}//使用int i =0;for(i =0; i <10; i++){*ptr = i;
ptr++;//改变指向,p最后指向空间的结尾后面位置}//释放free(p);
p =NULL;//及时置空
ptr =NULL;//最好这样,防止把ptr误用return0;}
注意:
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字节,用来存储整形的空间:
intmain(){int* p =(int*)calloc(10,sizeof(int));if(p ==NULL){perror("calloc");//打印错误信息return1;}int i =0;//使用for(i =0; i <10; i++){*(p + i)= i;//不改变指向 }//释放free(p);
p =NULL;return0;}
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的正常使用:
intmain(){//动态开辟int* p =(int*)malloc(40);if(p ==NULL){return1;}//使用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;return0;}
这里有几个注意点,需要重点提一下。
2.4.2.1 注意点 1
一定要接收realloc的返回值。
首先,得了解函数调整内存的情况。不要不知所云就认为
realloc
不管什么情况都是以原先空间的基础上向后延伸.
一定要返回值接收,否则当开辟空间足够大,返回新空间的地址时,如果我们不用返回值接收,就像这样:
intmain(){int* p =(int*)malloc(8000);if(p ==NULL){return1;}//使用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;return0;}
运行一下:
分析:
程序直接奔溃了,因为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置空,防止误操作。
intmain(){int* p =(int*)malloc(40);if(p ==NULL){return1;}int* ptr =(int*)realloc(p,80);if(ptr ==NULL)//判断{
p = ptr;
ptr =NULL;//置空}return0;}
2.4.2.3 注意点 3
当第一个参数为NULL空指针时,realloc起到和malloc/calloc一样的作用。
intmain(){int* p =(int*)realloc(NULL,40);//等价于malloc(40)return0;}
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成对出现也可能写不出正确的代码。
举个例子:
inttest(){int* p =(int*)malloc(40);if(p ==NULL){//...return1;}//使用if(1)//某个条件满足{return2;//条件满足返回}//释放free(p);//没有释放
p =NULL;return0;}intmain(){test();return0;}
分析:
这里malloc和free成对出现,但是由于满足条件,函数提前结束了,然后p指向的空间就没有释放,依然错误。
而p又是在函数中创建的,等函数结束,p也销毁,也并没有返回值来记住p,p在函数中指向的那块空间是被开辟的,但是出了函数就没人知道这块空间在哪里,这就造成了内存泄漏。
3. 常见的动态内存错误
动态内存虽然好用,但是使用不当就会让人十分苦恼,下面列出几个常见的错误。
3.1 对NULL指针进行解引用操作
intmain(){int* p =(int*)malloc(INT_MAX);if(p ==NULL)//判断{perror("malloc");return1;}else{*p =5;}free(p);
p =NULL;return0;}
分析:
如果开辟空间过大,malloc开辟空间失败,返回NULL空指针,这时对指针解引用操作,程序就会奔溃。
最好的方法是对p是否为空指针进行判断,如果为空指针则打印错误信息,并退出函数。
3.2 动态开辟空间的越界访问
intmain(){int* p =(int*)malloc(20);if(p ==NULL){return1;}//使用int i =0;for(i =0; i <20; i++)//把20当做元素个数了{*(p + i)= i;//严重越界}//释放free(p);
p =NULL;return0;}
分析:
malloc开辟了20个字节的空间,但是我误以为20为元素个数,造成了严重的越界访问,导致程序奔溃。
一定要搞懂函数的意思,在对指针进行操作时候看清楚!!!
3.3 对非动态开辟的内存使用free释放
intmain(){int num =10;int* p =#//...free(p);
p =NULL;return0;}
分析:
平时创建的局部变量在栈空间上开辟,当作用域结束,变量会自动销毁。而free只作用于在堆区上开辟的空间,如果将平常开辟的内存进行释放,程序会奔溃。编译器会很凌乱,表示这届程序员真难带!!!
3.4 使用free释放一块动态开辟内存的一部分
intmain(){int* p =(int*)malloc(40);if(p ==NULL){return1;}int i =0;for(i =0; i <5; i++){*p = i;
p++;//p改变了}//释放free(p);
p =NULL;return0;}
分析:
p在使用过程中,进行了调整,p不再指向原来动态内存开辟的空间的起始位置。这块空间可能是动态开辟内存的一部分,也可能完全不适于开辟的空间。这时运行程序,程序依然会奔溃。
3.5 对同一块动态内存多次释放
intmain(){int* p =malloc(40);if(p ==NULL){return1;}int i =0;for(i =0; i <5; i++){*(p + i)= i;}free(p);//已经释放过了//p = NULL//加上这个就不会奔溃//...继续写代码free(p);//忘记已经释放过了return0;}
分析:
当我们对一块动态内存进行释放后,接着写代码,然后忘记自己已经对这块空间进行释放。于是我们继续释放,当程序运行起来时,程序会奔溃。
要牢记一个malloc/calloc对应一个free。
如果我们在这里把
p = NULL
,就不会有问题了。因为free对空指针时不会操作的。
3.6 动态开辟内存忘记释放(内存泄漏)
//函数会返回动态开辟空间的地址,记得在使用之后释放int*get_memory(){int* p =(int*)malloc(40);//...return p;}intmain(){int* ptr =getmemory();//使用//没释放return0;}
分析:
函数返回了动态内存开辟的空间,我们可以对其进行使用。但是一定要释放,否则就会出现内存泄漏,也就是"吃内存"的情况。
在我们设计这个函数时就应该写好相应注释,提醒使用者。使用者也应该养成良好的习惯,对动态内存开辟的空间进行释放。
4. 几个经典的笔试题
4.1 题目1
下列程序运行结果是什么?
voidGetMemory(char*p){
p =(char*)malloc(100);}voidTest(void){char*str =NULL;GetMemory(str);strcpy(str,"hello world");printf(str);}intmain(){Test();return0;}
运行结果:
分析:
程序奔溃了。这里有两个问题。
第一个问题:
GetMemory
函数传参传str本身,p是str的一份临时拷贝。在函数中使用p开辟一块100个字节的动态内存的空间。也就相当于p改变了,但是str本身并没有改变。
回到Test函数中,str依然是NULL空指针。这时对str进行字符串拷贝。会对空指针进行解引用操作。程序奔溃。
值得一提的是这里的
printf(str)
并没有问题。可以通过一个简单的例子来证明:我们平时可以通过
printf("hello")
把hello打印出来,同样的我们也可以把字符串的首元素地址放入指针中,通过指针打印出字符串。因为
printf("hello")
是把h的地址传给了printf,这样打印没问题,那么我把其他部分省略,我的意思也是把地址传给printf,然后直接打印字符串。
第二个问题:
malloc开辟的空间没有释放。但是如果我们想释放也无法释放,因为在
GerMemory
函数中存放开辟空间地址的指针由于退出函数被释放了。返回
Test
函数后没人知道这块空间在哪里,也没法释放。
所以这个函数实际上是存在着很严重的问题的,所以我们接下来就将其改对。
正确写法:
- 传址调用,直接改变str
voidGetMemory(char** p){*p =(char*)malloc(100);}voidTest(void){char* str =NULL;GetMemory(&str);strcpy(str,"hello world");printf(str);//释放free(str);
str =NULL;}intmain(){Test();return0;}
分析:
我们要改变str的值,那么就把str的地址传入。str是一级指针,那么&str就需要用二级指针接收。通过解引用,找到str,将动态开辟空间的首地址放入p中。在使用完之后对空间进行释放。
- 参数无意义,返回值改变str
char*GetMemory(char* p){
p =(char*)malloc(100);return p;}voidTest(void){char* str =NULL;
str =GetMemory(str);strcpy(str,"hello world");printf(str);//释放free(str);
str =NULL;}intmain(){Test();return0;}
分析:
这种写法也行。但是这里的参数其实没有实际的意义,我完全可以省略参数,在函数体内创建变量,开辟动态空间,然后返回起始地址。这种方法也行,但是我不是很推荐。
以上两种方法运行结果:
4.2 题目2
下列程序运行结果是什么?
char*GetMemory(void){char p[]="hello world";return p;}voidTest(void){char* str =NULL;
str =GetMemory();printf(str);}intmain(){Test();return0;}
运行结果:
分析:
str里存放的是空指针,然后调用
GetMemory
函数,在函数中在栈空间上开辟一个数组,返回p(指向数组首元素位置)。但是出了函数,在函数中开辟的数组就会被销毁。当str接收返回值时就会接收被销毁空间的地址,当我对str进行打印时,这块空间已经还给操作系统了,这块空间可能被更改,也可能没更改。当前我们对其进行打印是乱码。
这就是典型的返回栈空间地址的问题!!!
一个小细节:
刚刚说返回函数栈空间的地址不对,那么这个函数对不对?
inttest(){int a =10;return a;}intmain(){int ret =test();printf("%d\n", ret);return0;}
分析:
这个函数是完全正确的。当局部变量a返回时,会把a放到寄存器中,假设我们这个寄存器为eax,然后a销毁。再通过eax把返回值带回。
那么这个呢?
int*test(){int a =10;return&a;}intmain(){int* p =test();printf("%d\n",*p);return0;}
分析:
这就是典型的返回函数栈空间的地址。
我主函数中的p指向的空间a已经被释放,属于野指针,如果通过指针去访问,就是非法访问。
但是大家可能会有疑惑,那我这个运行结果怎么解释:
这个其实是巧合。a所在的空间恰好没有被修改,如果我们坚信这个是对的,以后肯定是会翻车的!!!
如果我稍加改变,在打印
*p
之前打印一句话,例如这样:
intmain(){int* p =test();printf("hello\n");printf("%d\n",*p);return0;}
这里仅仅是增加了一句话就改变了
*p
的值,这是为什么?
看过我之前函数栈帧博客的,可能好理解些,接下来简单说一下原理:
当我们调用test函数时,在main函数上方需要开辟test函数的函数栈帧。栈空间使用习惯是从高地址向低地址使用。首先在栈帧最下方开辟a变量所需空间,当返回时则将
*p
放入寄存器中,将值带回,test函数栈帧被销毁。这一时刻很巧,a的值也没有改变。但是如果我们在打印
*p
前再使用了printf函数来打印一句话。这个printf函数可能就会在原先被释放的test函数栈帧的基础上开辟栈帧空间,这时a空间中的数据可能就会被修改,这就是6的来源。
4.3 题目3
下列程序运行结果是什么?
voidGetMemory(char**p,int num){*p =(char*)malloc(num);}voidTest(void){char*str =NULL;GetMemory(&str,100);strcpy(str,"hello");printf(str);}intmain(){Test();return0;}
运行结果:
分析:
str起始为NULL,将&str和开辟空间大小传给
GetMemory
函数,函数在内部通过
*p
找到str空间,将动态开辟空间的起始地址放入str中,在通过strcpy进行拷贝,拷贝也成功了,最后打印也没问题。
这个过程看似一气呵成,但是缺了释放动态开辟的空间!!!及时释放非常重要!!!
正确写法:
voidGetMemory(char** p,int num){*p =(char*)malloc(num);}voidTest(void){char* str =NULL;GetMemory(&str,100);strcpy(str,"hello");printf(str);free(str);
str =NULL;}intmain(){Test();return0;}
运行结果:
4.4 题目4
下列程序运行结果是什么?
voidTest(void){char* str =(char*)malloc(100);strcpy(str,"hello");free(str);if(str !=NULL){strcpy(str,"world");printf(str);}}intmain(){Test();return0;}
运行结果:
分析:
malloc开辟了一块100个字节的空间,放入str中,strcpy也将"hello"放入了str中,然后我就对str空间进行释放了。但是我没有置为空指针。所以下面的if语句是会执行的,这时我使用了被释放的str,str为野指针,为非法访问。在对str进行strcpy将world放入str中,再进行打印。
虽然跑出了结果,但是它本质上是错的,只能说明编译器大意了,没有闪(doge)。我们还是要发挥主观能动性,自己发现错误,毕竟我们是程序员。
正确写法:
这个代码其实槽点挺多的,首先它释放空间后没有置空。其次它也没有开辟完空间就对str是否为空指针进行判断,所以我们不妨对它进行一个大整改。
我们在释放完空间之后直接将str置为空指针。让下面的if语句起到作用,就达到了我们原本的目的。
voidTest(void){char* str =(char*)malloc(100);if(str ==NULL){return;}strcpy(str,"hello");free(str);
str =NULL;if(str !=NULL){strcpy(str,"world");printf(str);}}intmain(){Test();return0;}
运行结果:
5. 结语
到这里,本篇博客就到此结束了。相信大家对动态内存管理也有了一定的了解。动态内存管理在C语言中是一块非常重要的知识,还是希望大家可以熟练掌握。
如果觉得anduin写的还不错的话,还请一键三连!如有错误,还请指正!
我是anduin,一名C语言初学者,我们下期见!
版权归原作者 进击的安度因 所有, 如有侵权,请联系我们删除。