> 文档中心 > 【了解程序内存的四个分区、掌握堆内存管理函数的使用】(学习笔记17--内存管理函数)

【了解程序内存的四个分区、掌握堆内存管理函数的使用】(学习笔记17--内存管理函数)

目录

  • 内存的申请分配
  • 堆内存的释放
  • 堆内存的重新申请分配
  • 程序的内存大致可以分为四个部分:代码区、静态区、堆和栈
  • 程序的二进制码会存储在代码区
  • 程序中所使用的全局的、静态的对象以及常量等都存储在静态区
  • 局部的非静态对象存储在栈中
  • 堆是由程序员进行管理的一块内存区域,若要在堆中存储对象或数据,首先应通过内存申请分配函数进行堆内存空间的申请,当对象或数据不再被使用时,同样应调用相应的内存回收函数来完成对堆内存空间的回收,使用相关的内存管理函数,需要包含stdlib.h头文件

堆内存的申请分配

在堆中申请分配内存空间,有两个相关的库函数:malloc和calloc

(1)malloc函数

malloc函数的原型

void *malloc(size_t size);

函数只有一个参数size,它是size_t类型的,即无符号的整型数。函数返回值为void类型的指针。函数的功能就是在堆中申请参数size所指定字节数的一段内存空间,如果成功,则返回该段内存空间的首地址,即第一个字节所对应的内存地址:如果失败,则返回空指针

void *p = malloc(4);

在堆中申请分配了内存之后,就可以使用这块内存来存储数据了,先是通过if语句判断指针p是否为空指针,如果不是空指针,则通过指针p来访问在堆中所分配的4字节内存,并将其赋值为整数值100。由于指针p是void类型的,因此需要先将它转换成int类型的指针,然后再通过解引用运算符来访问并进行赋值

if(p != NULL)*(int *)p = 100;

代码中是将堆中所申请分配的4字节内存视为整型变量的存储空间来使用。因此可以在申请分配堆内存时,就对malloc函数所返回的指针进行转换,将mallloc所返回的void类型的指针,强制转换为int类型的指针,然后初始化给指针变量p

int *p = (int*)malloc(4);

由于整型变量的存储空间大小并非固定,在不同的编译器和系统平台上可能会具有不同的值,因此,使用sizeof运算符来获取整型变量的存储空间大小,并根据该值来申请分配堆内存空间,是个更好的选择

int *p = (int*)malloc(sizeof(int));

下面访问堆中所申请分配的内存并对其进行赋值

if(p != NULL)*p = 100;

也可以在堆中申请分配一个数组的内存空间

int *p = (int*)malloc(sizeof(int) * 5);

通过malloc函数在堆中申请一个大小为sizeof(int) * 5的内存空间,即可将所申请分配的内存空间视为一个长度为5的int类型数组的内存空间。并将返回值转换为int类型的指针初始化给指针变量p,即指针p指向了堆中的长度为5的int类型数组的首地址

下面就通过该指针,利用循环来访问堆中数组的各元素并赋予新值
在for循环语句的循环体中,首先对指针p进行运算,依次产生指向数组各元素的指针,然后通过解引用访问各元素,并对其进行重新赋值

if(p != NULL){for(int i = 0;i < 5;++i)*(p + i) = i;}

也可以采用数组下标的形式来完成

if(p != NULL){for(int i = 0;i < 5;++i)p[i] = i;}

也可以在堆中申请分配一个结构体大小的内存空间,并将该内存空间当作结构体变量一样来使用

typedef struct Stu{char name[20];int age;float score;}STU;

下面在堆中申请分配一个该结构体大小的内存空间
通过sizeof运算符获取结构体类型STU的大小,并根据该大小申请分配堆中的内存空间,并将该内存空间的首地址初始化给STU类型的结构体指针变量pstu。可以将在堆中所申请分配的内存空间视为一个STU类型的结构体变量,而pstu是指向该结构体变量的指针

STU *pstu = (STU*)malloc(sizeof(STU));

下面通过指针pstu来访问该结构体变量的各成员并进行赋值操作

strcpy(pstu->name,"zhangSan");pstu->age = 22;pstu->score = 85.5f;

再通过指针pstu来访问并打印输出所有成员

printf("Name:%s\n",pstu->name);printf("Age:%d\n",pstu->age);printf("Score:%.2f\n",pstu->score);

结果

Name:zhangSanAge:22Score:85.50

(2)calloc函数

函数原型为
该函数有两个参数,都是size_t类型。第一个参数num用于指定对象的数量,即calloc函数可以在堆中申请分配能存储指定数量对象的内存空间,第二个参数size用于指定对象的大小。因此,它非常方便为数组类型的对象开辟内存空间

void *calloc(size_t num,size_t size);

该语句的功能为,通过calloc函数在堆中申请分配5个int类型大小的内存空间,即可视为一个长度为5的int类型数组的内存空间。函数返回的也是指向堆中所申请分配的内存首地址,为void类型的指针,因此,通过强制类型转换将其转换为int类型的指针初始化给指针变量p

int *p = (int*)calloc(5,sizeof(int));

另外,使用calloc函数还有一个特别之处,就是能够对堆中所申请分配的内存空间进行默认初始化,即将内存空间的各字节的值都初始化为0,这是malloc函数所不具备的

int *p1 = (int*)malloc(sizeof(int));int *p2 = (int*)calloc(1,sizeof(int));printf("*p1 = %d\n",*p1);printf("*p2 = %d\n",*p2);

结果

*p1 = 7219104*p2 = 0

指针p1所指向的内存空间是通过malloc函数申请分配的,它不会对内存空间数据进行初始化,因此,对应的内存空间的数据为一个随机值。而指针p2所指向的内存空间是通过calloc函数进行申请分配的,会对内存空间的数据进行初始化,因此,对应内存空间中的数据的值为0

堆内存的释放

堆是由程序员来管理的一块内存区域,它的大小并非是无限的,如果不断地进行申请分配,总有将堆内存空间耗费殆尽的时刻。就像一个停车场,如果只有车停进来,而没有车开出去,则该停车场迟早会应所有的停车位被占满而无法继续提供服务。因此当堆中的对象或数据不再使用时,要及时将其所占用的内存释放回收,就像车被开走,而让停车场重新获得空的停车位一样

释放堆内存的函数为free,函数原型为

void free(void *ptr);

free函数没有返回值,参数ptr是一个void类型的指针。函数功能为释放参数ptr所指向的一段堆内存空间。可以将malloc或calloc函数所返回的指针作为实参进行free函数的调用

int *p = (int*)malloc(sizeof(int));free(p);

在调用free函数之后,参数指针所指向的堆内存就会被释放回收。因此,不应该再通过指针来访问或修改内存区域的数据

int *p = (int*)malloc(sizeof(int));free(p);*p = 100;

在调用free函数后,指针p所指向的堆内存已经被释放,可以认为此时的指针p指向了一片未知的内存区域,通常将这种指向未知内存区域的指针,称为野指针或者迷途指针。如果对野指针进行解引用,从而访问和修改内存数据,就会产生不确定行为,导致程序出现错误结果或者引发异常,并且这种错误调试起来也相对困难因此应予以杜绝。最好的办法就是,在释放堆内存后及时地将指针设置为空指针

int *p = (int*)malloc(sizeof(int));free(p);p = NULL;*p = 100;

在释放堆内存后,将指针p的值赋为NULL,即指针p成为空指针。后面再对指针p进行解引用并修改其值为100时,就会引发程序运行时错误

在进行堆内存释放的时候,还有两点需要注意

(1)释放要准确

释放的只能是堆内存,也就是调用free函数时,参数应该是一个指向堆内存的指针,不要对其它内存空间进行释放操作

int a = 100;int *p = &a;free(p);

指针p指向的是变量a,而变量a是存储在栈中的,栈中内存的申请分配和释放都是由系统自动管理的,因此,不要使用free来释放栈中的内存

(2)释放要及时

当堆中对象或数据不再使用时,要及时释放,防止内存泄露现象发生。所谓内存泄露,栈中内存被占用而无法被释放

int *p = (int*)malloc(sizeof(int));p = NULL;free(p);

malloc函数在堆中申请分配了int类型大小的内存,并将返回值初始化给指针变量p,即指针p指向了在堆中所申请分配的内存。第二条语句将指针p重新赋值为NULL,即将指针p设置为空指针。此时,没有任何指针指向在堆中所申请分配的内存,因此这块内存就会一直被占用而无法被释放回收,导致内存泄露现象发生。第三条语句虽然将指针p作为参数来调用free函数,但由于此时的指针p是一个空指针,因此,是不会释放回收任何内存的

堆内存的重新申请分配

在堆中申请分配内存空间后,有可能需要调整内存空间的大小,即对堆内存进行重新申请分配。把short类型扩大为int类型或者从1000字节缩小为500字节

可以使用函数realloc对堆内存进行重新申请分配,该函数的原型为

void *realloc(void *ptr,size_t size);

参数ptr是void类型的指针,指向堆中所申请分配的内存,应该是之前调用malloc、calloc或其它realloc函数所返回的指针。参数size为新的大小。函数的功能为将ptr所指向的堆内存空间调整为size大小。函数返回值为指向重新申请分配后的堆内存的指针,若重新申请分配失败,则为空指针

void *p = malloc(4);p = realloc(p,12);free(p);

这段代码中,首先通过malloc函数在堆中所申请分配4字节的内存空间,并由指针p指向该4字节内存;随后,再次调用realloc函数,以指针p作为第一个实参,整型常量值12作为第二个实参,这会对指针p所指向的堆内存进行调整,即将原先4字节的内存扩大为12字节内存。由于reallloc函数的返回值被再次赋值给指针p,因此,指针p此时指向的是堆中所分配的12字节的内存。最后,通过free函数所释放回收的即是堆中所分配的12字节的内存

上段代码有内存泄露的风险:假如在重新申请分配12字节内存时没有成功,则会返回空指针,而原先所申请分配的4字节内存并没有被释放回收。此时,指针p已是空指针,原先的4字节内存已经没有任何指针来指向,因此,无法通过free来进行释放回收,造成了内存泄露

对realloc函数的重新分配规要有所了解,这段代码中,是想将4字节的堆内存扩大为12字节,正常情况下,realloc函数会在原4字节的内存的基础上,再将其之后的8字节内存合并到一起,组成一个12字节的内存空间,但这有一个条件,原4字节内存之后的8字节内存必须是空闲状态。若在这8字节内存中出现部分或全部被占用的情况,则realloc函数会重新寻找一块新的连续的12字节内存,并将原4字节的内存释放回收,返回指向新的12字节内存的指针;若是没有寻找到连续的12字节内存,则会返回一个空指针,原先的4字节内存并不会被释放回收

要想杜绝内存泄露的风险,可做如下修改

void *p = malloc(4);void *ptmp = realloc(p,12);if(ptmp != NULL)p = ptmp;free(p);

定义一个临时指针ptmp,用来接收realloc函数的返回值。若重新申请分配成功,则ptmp指向了新申请分配的12字节内存,然后,在if语句中将ptmp重新赋值给p;若重新申请分配失败,则ptmp为空指针,if语句的条件不成立,不会将ptmp重新赋值给p,所以指针p依然指向原来的4字节内存。结果就是:无论重新申请分配是否成功,指针p都会正确地指向堆中内存,free函数都会正确地将堆中内存释放回收,因此不会造成内存泄露