C语言-动态内存管理

发布时间:2024-01-16 09:30

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.需要访问的空间若在内存中连续,则访问速度较快,因为所有程序都要提取到寄存器中再运行后取出,柔性数组提高访问速度。中间没有运用到的空间叫做内存碎片,使用柔性数组可减少内存碎片。

ItVuer - 免责声明 - 关于我们 - 联系我们

本网站信息来源于互联网,如有侵权请联系:561261067@qq.com

桂ICP备16001015号