这篇文章,我们一起来学习C语言中的动态内存管理!!!
1.为什么存在动态内存分配
我们先来想一下,我们现在掌握的开辟内存的方式是什么:
是不是就是直接创建一个变量或者数组,然后操作系统给我们分配空间:
int main() { int val = 20;//在栈空间上开辟4个字节 int arr[10] = { 0 };//在栈空间上开辟40个字节的连续空间 return 0; }
大家思考一下这样的方式有没有什么弊端:
我们这样定义一个数组int arr[10],开辟的空间大小是固定的。
int arr[10]就只能存的下10个整型,我们想多存一个都不行。
我们想存11个整型,用int arr[10]这个数组就不行了,除非我们再定义一个数组。
其次:数组在声明的时候,需要指定数组的长度,它所需要的内存在编译时分配。
但是,对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道。
那这时候,这样开辟空间的方式就不行了。
这个时候就需要动态开辟内存空间了。
那怎么实现动开辟内存呢?
C语言给提供了一些函数使得我们可以实现对内存的动态开辟。
2.动态内存函数的介绍
接下来我们就来一起学习一下这些函数:
2.1 malloc
看一下它的参数:
void* malloc (size_t size);
那它是用来干嘛的呢?
接下来再来给大家详细解释一下:
- 参数
size_t size
接收我们想要开辟的内存空间的大小,单位是字节,返回指向该内存块开头的指针。
int main() { void* p = malloc(40); return 0; }
返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
malloc给我们返回的指针类型是void*,但我们知道void*是不能直接解引用的,注意使用时要先转换为我们需要的指针类型。
比如我们想再申请的空间里放整数,就应该这样搞:
int* p = (int*)malloc(40);
然后,我们就可以往里面放整型数据了。
当然,你想用来放其他数据,就转换成其它相应的类型。
如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
当然用malloc开辟空间也有可能开辟失败,当请求失败的时候,它将会返回空指针(NULL)。
我们知道空指针是不能直接解引用的。
所以,对于malloc的返回值,使用之前,我们一定要检查一下。
如果为空,那就是失败了,就不能使用了。
那什么时候又可能失败呢,比如当我们开辟的空间特别大的时候,就有可能失败返回空指针。
如果开辟失败我们可以做一个相应处理,打印一下错误信息,然后return一下,让程序结束。
int* p = (int*)malloc(40); if (p == NULL) { printf("%s\n", strerror(errno)); return 1; }
函数strerror
我们在之前的文章里介绍过。
当然我们也可以断言一下:
assert(p);
如果不为空,那就是开辟成功了。
开辟成功,我们就可以使用了。
举个例子,我们现在就在上面开辟好的P指向的40字节的空间里放一些整型数据。
int i = 0; for (i = 0; i < 10; i++) { *(p + i) = i; }
40个字节,我们可以放10个整型,0到9。
我们也可以通过内存观察一下:
使用前:
这里再给大家提一点:
我们发现开辟好的空间里面放的这些其实是一些随机值
这也是malloc的一个特性:
- 新分配的内存块的内容不做初始化,仅保留不确定的值。
使用后:
如果参数size_t size为0,则返回值取决于特定的库实现(它可能是也可能不是空指针),但返回的指针不应被解引用。
此时malloc的行为是标准是未定义的,取决于编译器。
所以我们尽量不要这样试,况且这样做也没什么意义,申请一个大小为0的空间?
那申请的空间使用完之后,我们是不是什么都不用管了呢?
不是的,对于像malloc这些函数动态开辟的内存,使用完之后我们是需要将这些空间释放掉的,不及时释放,有可能会造成内存泄漏。
那怎么释放呢?
2.2 free
C语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的。
接下来我们就来一起学习一下函数free:
它的参数是这样的:
怎么用呢?
- 参数
void* ptr
接收一个指针,这个指针指向我们使用malloc这些动态开辟内存函数分配的内存块,无返回值。
比如,上面例子中的指针P:
int* p = (int*)malloc(20); /*if (p == NULL) { printf("%s\n", strerror(errno)); return 1; }*/ assert(p); int i = 0; for (i = 0; i < 10; i++) { *(p + i) = i; }
在上述循环的过程中,p 的指向并没有发生改变,还是指向分配的内存块的起始地址,所以我们就可以这样做:
free(p);
这样,就把malloc
申请的空间释放掉了。
那释放掉之后,是不是就万事大吉了呢?
不,我们还应该做一件事情:
把
p
置空
p = NULL;
为什么要这样做呢?
大家想一下,我们现在虽然已经把p指向的那块空间给释放掉了。
但是,p是不是还保存着那块空间的地址啊。
那么一个指针指向了一块被释放掉的空间,那它是不是一个典型的野指针啊。
要知道如果对一个野指针解引用那程序就会出错的。
如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
也就是说参数 ptr 指向的空间必须是动态开辟的。
如果指向其它的空间,那么free函数会怎么处理是标准未定义的。
比如:
int main() { int num = 10; int* p = # free(p); p = NULL; return 0; }
你写一个这样的代码,肯定是不行的,因为p指向的空间不是动态开辟的。
这里的num是一个局部变量,要知道局部变量是保存在栈区的,再来复习一下:
而我们这些动态开辟的内存,是堆区分配的。
- 如果参数 ptr 是NULL指针,则函数不执行任何操作。
像这样:
int* p = NULL; free(p);
函数不执行任何操作。
2.3 calloc
C语言还提供了一个函数叫 calloc , calloc 函数也用来动态内存分配。
我们一起来学习一下:
函数calloc 有两个参数,无返回值,那它的作用是什么呢?这两个参数分别接收什么呢?
函数的功能是为 num 个大小为 size 的元素开辟一块空间,同样返回指向该内存块开头的指针,类型为(void*)
参数size_t num接收我们想要分配空间的元素个数;
size_t size接收每个元素的大小,单位为字节。
那我们就可以这样用:
int main() { int* p = (int*)calloc(10,sizeof(int)); /*if (p == NULL) { printf("%s\n", strerror(errno)); return 1; }*/ assert(p); int i = 0; for (i = 0; i < 10; i++) { *(p + i) = i; } free(p); p = NULL; return 0; }
当然calloc分配的空间使用完也应该使用free释放并将指向空间起始地址的指针置空。
与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。
对于malloc 来说,它不会对开辟好的空间初始化,里面放的是随机值。
但是,calloc 会把申请的空间的每个字节都初始化为0。
就拿上面那段代码,我们来调式看一下:
- 和malloc 一样,calloc 函数如果开辟内存块失败,则返回空指针void*。
所以对于calloc 的返回值,我们也有必要做一下检查,判断是否为空指针。
和malloc一样,如果参数size_t size为0,则返回值取决于特定的库实现(它可能是也可能不是空指针),但返回的指针不应被解引用。
标准未定义的,取决于编译器。
总的来说,malloc和calloc 区别不大:
1. calloc 会在返回地址之前把申请的空间的每个字节初始化为全0,而malloc不会,里面放的是随机值。
2. 它们的参数不同。
2.4 realloc
realloc函数的出现让动态内存管理更加灵活。
有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的使用内存,我们就要对开辟的内存的大小做出灵活的调整。
那 realloc 函数就可以做到对动态开辟的内存大小进行灵活的调整。
一起来学习一下:
两个参数分别接收什么呢?
void* ptr接收一个指针,该指针指向我们想要调整大小的内存块,当然这块内存块也应该是我们之前动态开辟的空间。
size_t size接收我们想要为内存块调整的新大小,以字节为单位。
返回值又是什么呢?
返回指向重新分配的内存块的指针
举个例子吧,我们再来看一段上面的代码:
int main() { int* p = (int*)malloc(40); assert(p); int i = 0; for (i = 0; i < 10; i++) { *(p + i) = i; } free(p); p = NULL; return 0; }
还是这段代码:
我们使用malloc申请了40个字节空间,放了10个整型。
那假设我们现在想再放10个整型,那原来的空间就不够用了,那我们现在就可以使用realloc 进行扩容。
怎么搞呢?这样写:
int* p = (int*)malloc(40); assert(p); int i = 0; for (i = 0; i < 10; i++) { *(p + i) = i; } int* ptr = (int*)realloc(p, 80); if (ptr != NULL) { p = ptr; ptr = NULL; } //使用 free(p); p = NULL;
变成这样,我们再中间又加了一些代码。
我们看到上面代码中我们扩容后返回的指针赋给指针变量ptr ,那为什么不直接给p呢?
因为,realloc开辟空间也有可能会失败的,它失败同样返回空指针。
所以我们先赋给ptr ,然后判断一下,不为空,再赋给p,让p继续管理扩容后的空间。
然后,不使用ptr ,最好将其也置空。
然后,没什么问题,我们就可以使用扩容后的空间了。
但是,在扩容的时候,又存在存在两种情况:
原地扩
什么时候是原地扩呢?
就还拿刚才的例子来说:
int* ptr = (int*)realloc(p, 80);
p原来指向的空间是40个字节,现在我们想要使用realloc将p指向的空间扩容为80个字节。
那这时realloc就会从原空间向后看,如果后面有足够大的空间能够再增加40个字节,那么realloc就会在原地向后扩容40个字节,使得p指向的空间变为80字节。
当然这样realloc返回的地址还是原来p指向的地址。
异地扩
那什么时候异地扩呢?
假设现在还是相把p指向的空间扩容为80个字节。
但是,原空间后面没有足够大的空间,那这时候怎么办?
这时候:
realloc会在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址,不再指向原空间。
而且:
realloc会将原空间的数据拷贝到新空间,并会将旧空间释放掉。然后返回指向该内存块起始地址的指针。
比如:
int* p = (int*)realloc(NULL, 40);
那这句代码就相当于:
int* p = (int*)malloc(40);
以上就是对这4个动态内存函数的介绍,它们包含的头文件都是#include
。