前言
在C语言中,如果要使用堆空间的内容的话,可以使用C语言提供的动态内存函数去开辟堆空间,供程序员去使用,在C语言中,提供 4个函数malloc realloc calloc realloc free ,其实free是为了释放分配的堆空间的空间,其他三个是申请堆空间;这里分享这些函数的基本使用,和一些常见的动态分配的错误分析!!!
为什么要有动态内存分配堆空间
我的理解:
首先我们内存本来就会划分好多个分区,就是为了方便内存管理,数据管理的,而在平时我们使用的局部变量全局变量都是存放在不同的空间的,我们不可能一直使用局部变量开辟的空间都是放在栈空间上,因为栈空间在设计时候给它是非常少的,通常只有几M左右,而此时,当我们频繁的使用栈空间,当达到超过了栈的最大容量时候,就会内存溢出。 而对于我们来说:我们使用内存空间很多时候都是不确定的,不确定我要多少空间,不确定我要在哪里开开辟,所以当我们在栈空间开辟空间时候,并不够自由,而在动态内存分配时候,当我们写程序写着写着时候,觉得需要分配内存时候,就可以自由的分配内存,并且按自己的意愿分配大小。
在C语言的动态内存函数中,一直都会被人诟病,动态分配后的内存,不自己手动free,就会导致内存泄漏; ,其实从另一个方面来讲,既然C语言都给你提供了内存分配的使用权,当然也是会给你自己释放的使用权,它可以让你的内存使用更加自由,对内存的使用权完全掌握在自己的手里,你不觉得这样很“自由”嘛
看看在栈开辟一定大小的空间,仅仅就1M发生了栈溢出(vs2013编译器),所以我们很需要动态内存分配堆空间来使用
#include <stdio.h>
#include <stdlib.h>
int main()
{
char a[1024 * 1024];
system("pause");
return 0;
}
malloc 的使用 和 free
C语言提供了一个动态内存开辟的函数:
void* malloc (size_t size);
这个函数向内存申请一块连续可用的空间,并返回指向这块空间的起始地址。
malloc 这个函数仅仅开辟内存空间,并不做初始化操作。
如果开辟成功,则返回一个指向开辟好空间的指针。
如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自 己来决定。
如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。
C语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的,函数原型如下:
void free (void* ptr);
free函数用来释放动态开辟的内存。
如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
如果参数 ptr 是NULL指针,则函数什么事都不做。
malloc和free都声明在 stdlib.h 头文件中。
使用案例:
#include <stdio.h>
int main()
{
int* ptr = (int*)malloc(10*sizeof(int));
if(ptr == NULL)
exit(1);
for(int i=0; i<num; i++)
{
*(ptr+i) = 0;
}
free(ptr);
ptr = NULL;
return 0;
}
如何正确理解:free(ptr) ?
free(ptr) :释放ptr指向的动态内存的空间; 可能总有人误解:是释放ptr指针,然后ptr指针的生命周期就结束了,ptr这个指针就是销毁了;其实不然;即使free了ptr,ptr还是指向动态分配的堆空间的其实地址,这个ptr指针的生命周期并没有结束,但是我们却不能够再使用ptr指向的内存空间了。
为什么 free(ptr) 后,就不可以再使用ptr所指向的空间了?
因为free ptr后,ptr指向的动态分配空间就被系统回收掉了,就表明,这段空间不属于你的了,系统收回去了,虽然你还有ptr指针指向那段空间,但是没用,你用ptr试图访问那段空间就会出错,内存访问失败,这是不被允许的行为的,所以,很多人就有一种做法,再free调用ptr指向的内存空间时候,为了防止别人再次使用 ptr 这个指针去访问被回收的空间,就会把它置为 ptr = NULL;
free (ptr) 后,ptr置为空,即 ptr = NULL ;是必要的行为嘛?
其实不是必要的,因为ptr指向的内存已经被系统回收了,系统就已经让你不能够再使用ptr了,系统规定你不能使用了,但是你还是要强制使用,那就是你的问题了,就像明知道国家法律,你要去犯法一样,后面的事情就是你要承担了的。
free(ptr),我们只知道ptr指向动态内存的起始地址ptr,但是我们却可以释放完动态分配的内存空间大小,换句话说,free 是如何知道动态内存分配的空间是多大的,它是如何根据起始地址就知道了要释放的内存大小?
其实,当我们进行动态内存分配的时候,并不是仅仅分配了你所需要的字节数的内存空间,malloc这个函数,还还会分配多一部分空间,这一部分空间是用来记录分配的字节数或其他一定量的信息。 可以理解:malloc分配空间时候,不仅仅分配了程序员所需要的空间,于此同时也分配了一段空间是用来存放内存信息的空间的。只不过这个用来存放内存信息的空间我们是不需要知道。 所以当free(ptr)时候,free就可以根据存放内存信息的空间内容,去精准的释放空间大小啦。
至于这个malloc用来存放信息空间是什么信息由于又是一个大话题,估计等我有缘再探讨它了,到时候,有源再分享这部分内容。
calloc函数
calloc函数也是动态内存开辟函数;看看它的原型
void* calloc (size_t num, size_t size);
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *p = (int*)calloc(10, sizeof(int));
if(NULL != p)
{
}
free(p);
p = NULL;
return 0;
}
realloc 函数
realloc函数的出现让动态内存管理更加灵活。 有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时 候内存,我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大 小的调整。 函数原型如下:
void* realloc (void* ptr, size_t size);
-
ptr 是要调整的内存地址;size 调整之后新大小; -
返回值为调整之后的内存起始位置;这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到 新 的空间。 -
realloc在调整内存空间的是存在两种情况: 情况1:原有空间的之后有足够大的空间 当是情况1 的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。
- 情况2:原有空间之后没有足够大的空间
当是情况2 的时候,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用,并把旧空间内容复制到新的空间内容上去,同时释放旧空间,这样函数返回的是一个新的内存地址。
对于 realloc函数来说,可能返回的指针地址和原来不一样,这点要注意。
常见的动态内存分配的错误使用
对空指针的使用
void test()
{
int *p = (int *)malloc(INT_MAX/4);
*p = 20;
free(p);
}
上面的情况可能会出问题:因为malloc分配内存时候,假如分配失败,就会返回空指针,所以在malloc分配完内存后,必须有一个操作,那就是对分配的指针做一个判断。
对动态开辟空间的越界访问
void test()
{
int i = 0;
int *p = (int *)malloc(10*sizeof(int));
if(NULL == p)
{
exit(0);
}
for(i=0; i<=10; i++)
{
*(p+i) = i;
}
free(p);
}
这种情况很明显是对动态分配得到的内存越界访问,也会出现很大问题,要避免。
对非动态开辟内存使用free释放
void test()
{
int a = 10;
int *p = &a;
free(p);
}
这种情况也是错误的:千万不要对不是动态内存分配的空间指针使用free;
使用free释放一块动态开辟内存的一部分
void test()
{
int *p = (int *)malloc(100);
p++;
free(p);
}
对于动态内存分配的起始地址指针,不要随便乱动它,它是有用的,用来记录内存的起始位置,方便下次不要时候可以用起始地址指针去释放改内存,如果你移动了起始地址的指针,那么就会发生错误,因为起始地址不见了,free就找不到正确释放内存的大小了。
void test()
{
int *p = (int *)malloc(100);
if(NULL != p)
{
*p = 20;
}
}
int main()
{
test();
return 0;
}
上面是最常见的错误了,忘记释放动态内存分配的空间,这会导致内存泄漏,内存泄漏就是使用完的内存没有释放;所以一定要记住,使用完的动态内存要记得释放,且要正确释放。
经典错误使用动态内存分配的代码分析
题目1
问:请问运行Test 函数会有什么样的结果?
先好好分析一下,然后去编译器验证你的想法,同时给出你的分析理由,假如可以的话,如何修改正确,你是否做得到呢?
void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
int main()
{
Test();
return 0;
}
来看看编译器执行的结果吧!直接报错。 首先:我们要理解这个程序的本意是什么:它是想通过GetMemory函数申请一个堆空间,用str指针指向该堆空间,然后对该str指针指向的堆空间赋值。
但是它这个想法却因为错误的使用的动态内存分配,导致了很严重的错误!
对于Test函数里的 GetMemory(str) 函数调用,这里是值传递方式,值传递方式就是直接把str的NULL值拷贝一份给 GetMemory(char* p) 函数里面形参p,所以p的值为NULL,在GetMemory函数里面的形参p申请了一份内存100个字节,p指针就指向了该100个字节的堆空间。当在Test函数里面调用完后GetMemory函数后,str的指向并没有发生改变,还是指向NULL,所以当指向strcpy(str, "hello world") ;这句代码时候,就相当于给NULL拷贝,然而对NULL指针是不可以访问的,一旦访问就会发生错误! 对于 GetMemory函数来说,还有一很严重的问题,那就是内存泄漏,一旦调用完GetMemory函数后,形参指针p就销毁了,p销毁后,你想找到动态分配的起始地址都找不到了,此时就会发生内存泄漏了。
所以总体描述上面的错误: 1. str给p传参时候,是值传递,值传递的方式就是str的一份拷贝数据给p,p和str是没有关系的,所以p申请得到的malloc空间只是对p有效,而对str是无效的,str依旧是NULL;一旦执行strcpy时候,就相当于给NULL拷贝数据,然后对NULL指针访问时不被允许的,所以程序就会发生崩溃错误! 2. 还有一个问题就是内存泄漏的,GetMemory函数执行完后,p形参就销毁了,而p执行的堆空间且没有被释放,就会发生内存泄漏;
所以假如我们想修改上面的程序正确的话,那么就可以抓住上面的两个问题修改 3. 值传递该为地址传递:由于实参是一级指针,所以传递一级指针实参地址给形参p时候,形参p的指针要设计是二级指针的形式 4. 释放堆空间GetMemory函数的堆空间:在使用使用完堆空间后(在这里就是strcpy函数执行完后),free(str)即可;重点错误做法:在GetMemory函数里面free§,这是错的,因为,你申请完的堆空间,都还没有使用就free了,那也会发生错误!
修改后正确的代码:正常打印 hello world;
void GetMemory(char **p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(&str);
strcpy(str, "hello world");
free(str);
str =NULL;
printf(str);
}
int main()
{
Test();
return 0;
}
题目2
请问下面的结果可能是什么?
char *GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
}
int main()
{
Test();
return 0;
}
你们分析完后,我们看看编译器的结果: 我们看到了随机值!!!!并且还有一个警告?返回局部变量的地址;没错这个问题真的很常见就是返回局部变量的地址的问题。
分析过程:
GetMemory 函数 里面的定义了一个局部变量 p数组,并且给p数组初始化了 “hello world”; 当return p 时候,由于数组名就是首元素的地址,就相当于return 了 hello world 首字符 h的地址回去给Test函数的str指针 ;一旦 GetMemory函数执行完后,局部变量的数组p就会被销毁,销毁的意思是:局部变量p的空间使用权权给了操作系统,而不属于程序本身,这意味这程序无法使用该局部变量p的空间了,而系统回收的这段空间他就会使用,所以访问它的话就不是你原本的想要的结果了 即使我们 GetMemory 函数返回了该内存的地址,调用完后 在Test函数也有 str指针接收该地址,即表明str指针是有资格有能力找到被系统回收的局部变量p的空间的,这个本身是没有问题的,但是但是:虽然str指针是有资格有能力找到被系统回收的局部变量p的空间的,但是str却没有使用该空间的权力,这就类比在酒店住房到期一样,虽然你到期了还可以找到房间在哪里,但是却没有住的权力了。
题目3
运行结果是什么?
void Test(void)
{
char *str = (char *) malloc(100);
strcpy(str, "hello");
free(str);
if(str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
int main()
{
Test();
return 0;
}
分析:
这个就很好理解了:上面Test函数里面free(str) 后,str就是野指针,就是野指针啦!后面还是继续使用str指针,很明显free后就是把str指向的堆空间还给操作系统,你却还要strcpy 拷贝world给 str指针,很明显是在尝试访问不属于你的空间的内存;这个肯定是会发生错误!
|