为什么要需要动态内存管理?
在初入C语言时,用数组就可以开辟一个我们需要的空间大小。那为什么要使用动态内存的形式来管理空间呢?直到我碰到编写通讯录这个任务时,我突然感觉数组对于未知开辟空间大小的任务来说有些疲软了。开辟空间大了,浪费空间;开辟空间小了,会越界。不能灵活的随着需要的数据去开辟对应大小的空间。
动态内存管理可以方便的解决这种问题,当需要输入新数据时,可以在堆区中开辟对应类型的空间大小去储存数据。当不需要的时候,可以释放这个空间。使用起来十分顺畅。
接下就要介绍开辟空间的函数:malloc和calloc函数。(调用库为<stdlib.h>)
为了方便讲解,释放空间函数不会在上面3个例子中出现。
在这里先强调一点,空间申请完不用后,一定要释放,并且释放后要把指向该空间的指针置为NULL。就像去开车一样,开完车门不关,出事的概率有多大,问题多严重???所以一定要释放空间,指针置NULL。
1.malloc函数
malloc函数只有一个参数,参数表示需要开辟空间的字节数,参数类型为size_t(无符号整形)。
返回值为void类型的指针,指针指向开辟空间的地址。(空间开辟失败返回NULL)
下面为malloc函数使用案例:
//void* malloc(size_t size)
#include<stdlib.h>
int main()
{
int* p1 = (int*)malloc(4);//开辟一个四字节的空间,由于malloc函数返回指针类型为void,需要(int*)进行强制类型转化。
int* p2 = (int*)malloc(sizeof(int));//开辟一个int类型大小(4字节)的空间,左右两边指针类型不一致,需要(int*)强制类型转换
return 0;
}
2.calloc函数
calloc函数有两个参数,第一个参数表示需要开辟几个元素的空间,参数类型为size_t(无符号整形)。第二个参数表示一个元素的空间大小,参数类型为size_t(无符号整形)。
返回值为void类型的指针,指针指向开辟空间的地址。(空间开辟失败返回NULL)
下面为calloc函数使用案例:
//void* calloc(size_t num, size_t size)
#include<stdlib.h>
int main()
{
int* p1 = (int*)calloc(2, 4);//开辟两个四字节的空间,由于calloc函数返回指针类型为void,需要(int*)进行强制类型转化。
int* p2 = (int*)calloc(2, sizeof(int));//开辟两个int类型大小(4字节)的空间,左右两边指针类型不一致,需要(int*)强制类型转换
return 0;
}
malloc函数和calloc函数的区别:malloc函数不会对申请的空间进行初始化为0,calloc函数会对申请空间进行初始化为0。
下面为两个函数开辟空间内容的截图:
(1)malloc:
(2)calloc
那上面两个开辟的空间不就和数组一样是固定的吗?不着急,下面就是动态内存管理的重点了。
用于调整申请空间的函数:realloc函数。
如果感觉申请的空间大了或者小了,都可以用realloc函数对其进行调整。调整方式可能和你想象的不一样。因为编译器不同,调整方式也有区别。
在VS2019的x86编译器下:如果调整空间比原空间大,原先的空间后面无法再开辟需要的空间,realloc函数会重新开辟一个符合要求的空间,并返回该空间的地址。当然,你的数据依旧会按照原先空间排列的顺序存储在新空间中。如果调整空间小于等于原空间,返回空间的地址不会变。
在VS2019的x64编译器下:无论是开辟多大的空间,只要使用realloc函数,它便会在堆区开辟一块新的且符合要求的空间,并返回该空间的地址。之前申请的空间内容不会变。如果调整空间比原空间大,realloc函数会重新开辟一个符合要求的空间,并返回该空间的地址。当然,你的数据依旧会按照原先空间排列的顺序存储在新空间中。如果调整空间小于等于原空间,realloc函数会重新开辟一个符合要求的空间,并返回该空间的地址。该空间所有数据与原空间从头开始对应的数据一致,直到新空间结束(也就是原空间从头开始拿数据塞到新空间中,到后面塞不进新空间的数据,从新开辟的空间来看,这部分数据丢失了)。
3.realloc函数
realloc函数拥有两个参数,一个为void类型的指针(指向需要调整申请空间的指针),一个为调整后的空间大小(也就是多少字节数),类型为size_t 。
返回类型为void类型的指针,指针指向开辟空间的地址。(空间开辟失败返回NULL)。
下面为realloc函数使用案例(基于VS2019的x64编译器):
//void *realloc( void *memblock, size_t size );
#include<stdlib.h>
#include<stdio.h>
int main()
{
int* p1 = (int*)calloc(2, sizeof(int));
*p1 = 0xFFFFFFFF;
*(p1 + 1) = 0xFFFFFFFE;
p1 = (int*)realloc(p1, sizeof(int)*4);//p1指向的空间需要调整空间大小为4个4字节(int类型大小)
*(p1 + 2) = 0xFFFFFFFD;
*(p1 + 3) = 0xFFFFFFFC;
p1 = (int*)realloc(p1, sizeof(int)*2);//p1指向的空间需要调整空间大小为2个4字节(int类型大小)
printf("%d", *(p1 + 1));
return 0;
}
(1)calloc函数开辟空间
第一次通过calloc开辟空间,并且将数据放进去,为了后面好观测其变化。第一次通过calloc函数开辟得到的空间首地址为0x0000 0224 F9FB F460(为了方便观测地址变化,将其进行分割)。
(2)realloc函数第一次开辟空间
第一次通过realloc函数对p1指针指向的空间进行空间调整,空间调整为4个int类型大小的空间(16字节),并且继续往后存放数据。可以看到在x64编译器下,地址发生了变化(在x86编译器下尝试时,地址没变)。地址为0x0000 0224 F9FB F780。
(3)realloc函数第二次开辟空间
第二次通过realloc函数对p1指针指向的空间进行空间调整,空间调整为2个int类型大小的空间(8字节)。可以看到在x64编译器下,地址发生了变化(在x86编译器下尝试时,这里的地址也没变)。地址为0x0000 0224 F9FB F690。观察上面的地址存放的数据可以发现,数据搬移只过来8字节的数据,后面的数据在新空间中都丢失了。
最后,讲的是如何对动态开辟的空间进行释放。这里就要提到上面一直强调的释放函数free。
4.free函数
realloc函数只有一个参数,参数为void类型的指针(指向需要释放动态内存空间的指针)。没有返回值。
//void free( void *memblock );
#include<stdlib.h>
#include<stdio.h>
int main()
{
int* p1 = (int*)calloc(2, sizeof(int));
*p1 = 0xFFFFFFFF;
*(p1 + 1) = 0xFFFFFFFE;
p1 = (int*)realloc(p1, sizeof(int) * 4);
*(p1 + 2) = 0xFFFFFFFD;
*(p1 + 3) = 0xFFFFFFFC;
p1 = (int*)realloc(p1, sizeof(int) * 2);
free(p1);//释放p1指向地址的空间(动态开辟的空间)
p1 = NULL;//让p1指针指向NULL,不然p1会变成野指针
return 0;
}
为什么p1要指向NULL?因为开辟内存相当于拥有钥匙,属于合法使用。当开辟的空间释放完,相当于非法占用钥匙,钥匙是需要归还的。当指针不知道指向什么地址时,指向NULL。
以上是对动态内存的初步介绍。
如果有不当之处,请大佬指正。