C语言中的动态内存管理主要依靠四个动态内存函数实现。
四个函数:malloc,calloc,realloc,free。使用这四个函数均需要#inlude
1.为什么存在动态内存管理方式呢?
若使用平常的创建数组,变量等方法申请空间,这些空间大小是固定的。若遇到需要增加/减少空间的实际需求时往往不能达到要求。所以衍生出更方便的,高效的(空间与时间使用率)动态内存管理方式。
2.内存中的区域
内存被划分为很多区域,每个区域的空间都存在不同的特性。
这里主要解析 栈区,堆区与静态区。
在栈区中,主要存放局部变量与函数的形式参数。
在堆区中,主要存放可供动态内存分配函数(malloc,calloc,realloc,free)作用的空间。
在静态区中,主要存放全局变量与静态变量。
函数:
malloc:void* malloc(size_t size)
size_t代表无符号整数。malloc在堆区中开辟一个size个字节的连续可用的空间,并返回这块空间的首地址,若堆区空间不足,则返回NULL。
注意:返回地址的类型为void*,若要使用这块地址应创建变量承接并强制类型转换。
若size为0,则malloc的行为未知,由编译器决定并解释。
举例:
int* p=(int*)malloc(10*sizeof(int));//开辟十个整型的空间并将首地址经强制类型转换后赋给p
if(p==NULL)
{
perror("malloc");//打印当前错误信息
return 1;//结束程序
}
之后p就可根据自身类型(int*)对这块空间进行访问。
而访问过后需要free(p); p=NULL; free(动态开辟内存的首地址)就能将这块动态开辟的空间释放,释放过后p相当于野指针(指向的地址无意义),所以需要赋NULL给p防止p可能对有用数据的空间造成改动。若不释放动态申请的内存空间,该内存会一直占据堆区中空间,导致内存泄漏。
calloc:void* calloc(size_t num,size_t size)
calloc在堆区中开辟num个size字节的连续可用的空间。与malloc不同,calloc会将开辟空间初始化为0(每个比特位的数据都为0).若开辟空间失败也返回NULL。
举例:int* p=(int*)calloc(10,sizeof(int));//开辟了十个整型的空间并初始化空间为0
realloc函数: void* realloc(void* p,size_t size)
从地址p开始向后开辟size个字节的连续可用的空间。
c-realloc在操作过程中是释放旧空间而且分配并返回新空间。
使用realloc时有两种情况:
1.若原先动态申请过空间的指针p之后有size个字节的空间,就直接在p之后开辟剩余的空间。
2.若原先动态申请过空间的指针p之后没有size个字节的空间,就在堆区中重新申请一个有size个字节的空间,然后返回这块空间的首地址。
申请空间失败失败则返回NULL。开辟的空间中内容也是未知的。
下面来看一下常见的动态内存管理错误:
1.对NULL进行解引用操作-操作失败
解决方法:对malloc的返回值进行一个排空操作,就是用if判断malloc返回值是否为NULL并进行对应处理
2.对动态开辟空间的越界访问
解决方法:自己检查代码
3.对非动态开辟空间进行free操作—此时free无任何作用并且报错
解决方法:检查代码
4.使用free释放了一部分动态开辟的内存(应该释放内存完全)
可能原因:一开始用来申请空间的指针值被改变,需检查代码
5.对同一内存多次释放
此时free无任何作用——或者可能会影响正常代码,需要检查。
6.动态开辟内存忘记释放(内存泄漏)
可能原因:使用如函数的局部变量申请空间之后该变量被删除,则此空间无法释放。
训练题目1:
void Fetmemory(char*p)
{
p=(char *)malloc(100);
}
void test(void)
{
char * str=NULL;
Getmemory(str);
strcpy(str,"helloworld");
printf(str);
}
结果:什么都不打印。
首先解释下printf(str);是什么意思:printf函数在运作时接收一个地址,然后从该地址开始往后打印字符,遇到'\n','\0'时停止打印(但转义的作用会表达出来)。所以printf(str)的写法是正确的,同样char s[]="asdfg"; printf(s); char* p="asd";printf(p)的写法都是对的。
一般的,printf会使用控制符来输出内容,接收到地址时运行机制有所差异。
不会打印hello world的原因:str为一个指针,里面存放的内容是NULL。在传参时,p并不是str,p只是str的一份临时拷贝,p的内容也是NULL,经过malloc之后p的内容变为一块空间的首地址,但str的内容还是NULL(也就是str之后并没有已申请的可以使用的内存),使得strcpy与printf无效。而p的内存并没有被释放,造成内存泄漏。
正确写法:
void getmemory(char** p)
{
*p=(char*)malloc(100);//此时*p代表指针str,str被赋予地址就可以进行操作了
}
void test(void)
{
char* str=NULL;
getmemory(&str);//将str的地址传过去,经过解引用可以准确对str赋值
strcpy(str,"helloworld");
printf(str);
free(str);
str=NULL;
}//当然test函数需要在main中使用。
题目2:
char* getmemory(void)
{
char p[]="helloworld";
return p;
}
void test(void)
{
char* str=NULL;
str=getmemory();
printf(str);
}
结果:不打印hello world,打印出来随机值。
原因:虽然str接收到了函数返回的地址并指向那块空间,但这是在函数调用之后,在函数中创建的数组空间已经没有访问权限(已经被释放)(即使str指向那块空间的首地址),所以强行使用的话会出现随机值,此时str是一个野指针(未初始化指向空间,或者指向空间无访问权限)
这种问题一般叫作返回栈空间地址的问题,栈空间上的地址一般不要轻易返回,该空间很容易被回收使用权限(栈空间容易被释放)。因为程序中有很多函数在运作,这块空间中的内容很容易被改为其他值。
内存中各区域。
柔性数组:结构体中的最后一个元素(成员)允许是大小未知的数组。(数组创建时不指定空间大小或者指定空间大小为0)
struct s1
{
int num;
double d;
int arr[];
}
struct s2
{
int num;
double d;
int arr[0]
}
这两种柔性数组创建方式都成立。
柔性数组的特点:
1.结构体中柔性数组成员之前至少有一个成员
2.sizeof计算结构体大小时不计算(包括)柔性数组成员的大小
3.若要在main中使用结构体指针开辟空间时,除了要开辟sizeof(结构体类型)的空间,还需要开辟预计的柔性数组所占的空间。
举例:
struct s3* ps=(struct s3*)malloc(sizeof(struct s3)+40);//40为柔性数组预期大小
for(i=0;i<10;i++)
{
ps->arr[i]=i;//柔性数组的使用与普通数组使用无异。
}
注意:有另一种“柔性数组”的实现:将柔性数组替换为对应类型的指针,在main中申请了结构体的空间之后用该指针为“柔性数组”申请空间。但请注意:用柔性数组申请的空间是连续的,用指针申请的空间不一定是连续的。释放内存时用指针的方法也需要使用free释放两块空间。但这两种方法都可以实现我们想要的效果。
如:
struct s4* p=(struct s4)malloc(sizeof(struct s4));
p->arr=(int*)malloc(10*sizeof(int));//假设arr为int*类型
释放时:free(p->arr); free(p);//释放两个申请的空间
柔性数组优点:
1.便于释放内存
2.需要访问的空间若在内存中连续,则访问速度较快,因为所有程序都要提取到寄存器中再运行后取出,柔性数组提高访问速度。中间没有运用到的空间叫做内存碎片,使用柔性数组可减少内存碎片。