一文带你了解C语言中的动态内存管理函数
目录
- 1.什么是动态内存管理
- 2.为什么要有动态内存管理
- 3.如何进行动态内存管理
- 3.1 malloc
- 3.2 free
- 3.3 calloc
- 3.4 realloc
- 总结
1.什么是动态内存管理
平时我们写代码,一种非常常见的写法是:
int a = 0; // 创建一个变量 int arr[10] = {0}; // 创建一个数组
当你创建变量a的时候,其实是向内存中申请了4个字节的空间来存放一个整数。而当你创建数组arr的时候,是向内存中申请了40个字节的空间来存放10个整数。当你这么写了之后,从a和arr创建开始,一直到程序结束,它们都只会占用4个和40个字节的空间,不会多也不会少!换句话说,它们的大小并不会变化,是静态的。
与之相反,如果你向内存中申请一块空间,这块空间可以一会大,一会小,你觉得不够了就扩容,你觉得空间太多了就缩容,你想要多少空间就来多少空间,这块空间的大小是会变化的,所以我们认为它是动态的。对这样可以改变大小的内存空间的管理,称之为动态内存管理。
你可能会想:这么神奇?我还能自己操纵向内存申请的空间大小?这是怎么做到的呢?别急,听我慢慢道来。
当你创建一个局部变量时,比如int a = 0;,变量a是存储在栈区上的。一般来说,如果你想要改变栈区上申请的空间的大小,是非常困难的,因为这块空间是编译器提前帮你算好的,一旦进入到函数内部就自动帮你开辟好了,我们作为程序员并没有太多的操作空间。但是内存中有一块空间,专门用来给我们做动态内存管理,这块空间就是堆区。如果我们在堆区上申请空间,就可以做到想要多少空间就来多少空间,不够了还可以继续申请,非常灵活。
2.为什么要有动态内存管理
静态的内存管理,某些情况下会显得很死板。比如,我要实现一个通讯录,是不是要创建一个数组来存储联系人的信息?那好,我要开辟多大的空间?如果容量是100人,假设我要存储200个人的信息呢?你可能会说,那好,给我1w个空间!那我如果只存3个人的信息呢?这么多的空间是不是就浪费了?
所以,静态的内存管理有2个硬伤:
- 开辟空间太少,不够用。
- 开辟空间太多,造成空间浪费。
而这2个问题可以用动态内存管理的方式完美解决。空间太少,咱再多来点!空间太多,我还可以缩容,减少空间消耗。
3.如何进行动态内存管理
这可能是大家最关心的问题。我应该怎么样进行动态内存管理呢?C语言提供了4个函数,专门用来进行动态内存管理。这四个函数分别是malloc, free, calloc, realloc。使用这几个函数都需要引用头文件stdlib.h。
接下来,我将详细介绍这几个函数的用法。
3.1 malloc
如果你想要40个字节的空间来存储10个int,你可以跟内存说:“喂,内存!给我40个字节的空间!我要用!”也可以直接使用malloc函数。malloc函数的声明如下:
void* malloc(size_t size);
其实你只需要给malloc传你需要的空间大小就行了。比如你想申请40个字节,就直接malloc(40);,此时malloc就会屁颠屁颠的跑去内存那里,申请40个字节的空间,然后返回这块空间的起始地址。有了前面“什么是动态内存管理”的讲解,你应该很清楚,这40个字节的空间是在内存的堆区上的。
注意,malloc返回空间的地址,类型是void*。啥是void*呢?其实,void*的意思是,malloc不知道返回的地址是什么类型的,这要你来告诉他。比如,你想用这40个字节的空间来存储10个int,你当然希望这个指针是int*类型的,此时你就会用一个int*类型的指针来接收。由于void*和int*毕竟是两种不同的类型,所以需要强制类型转换。具体的写法如下:
int* ptr = (int*)malloc(40);
此时你就有了一个指针ptr,指向了动态开辟的40个字节的连续空间了,你就可以在这块空间里为所欲为了。接下来,你还需要掌握一些细节。
malloc函数申请空间一定会成功吗?那可不见得!如果你一次申请的空间太多了,比如malloc(INT_MAX);,INT_MAX是整形变量能够存储的最大值,你一次申请这么多字节的空间,那很有可能失败呀!或者还有一种可能,你的电脑已经运行了几百个进程了,内存已经快被耗干了,哪怕你申请的空间不是很多,也有可能申请失败。
malloc函数在申请空间失败的时候,会返回NULL指针。每次使用malloc函数的时候,都必须要检验返回值是不是NULL,否则后面在使用这个指针的时候,有可能会有对NULL指针的解引用操作,这是非常危险的!检查的示例代码如下:
if (ptr == NULL) { // 错误处理 perror("malloc()"); exit(-1); }
错误处理的代码应该根据实际情况来写,我这里只是给了一个常见的处理方式:用perror函数报个错,接着直接用exit函数结束进程,非常简单粗暴。当你验完货,发现货的质量不对,直接扭头就走,不玩了,exit掉。而当你觉得这货的质量还可以,就可以使用了。
使用起来非常简单,ptr指向了这块空间的起始地址,由于类型是int*,+1就跳过一个int,+2就跳过2个int,以此类推。每次解引用,就可以访问这块空间了,比如把1~10放进去:
for (int i = 0; i < 10; i++) { *(ptr + i) = i + 1; }
由于在C语言中,*(a+b)就等价于a[b],所以上面的代码也可以这么写:
for (int i = 0; i < 10; i++) { ptr[i] = i + 1; }
有没有发现,ptr就像一个数组一样,这个数组的容量是10个int。
3.2 free
注意,动态内存管理的空间需要程序员手动释放!前面我们用malloc开辟了一块空间,并把这块空间的起始地址交给ptr指针来看管,最后,我们还要使用free函数来释放这块空间。使用方式非常简单,你想释放谁就free谁,比如:
free(ptr);
传给free函数的指针必须指向一块动态开辟空间的起始位置!言外之意就是,你malloc出来一块空间交给ptr管理后,你还能修改ptr吗?答案是:不行。你一旦把ptr给改了,就找不到这块空间的起始位置了,就没办法把它free掉了。就相当于,警察局有一个卧底,这个卧底和一个领导是单线联系的,如果领导被干掉了,就没有人知道这个卧底的身份了。ptr就是这个领导,malloc开辟出来的这块空间就是这个卧底。
你可能会想:如果我不free,会怎么样呢?事实上,如果程序员没有手动回收动态申请的空间,当程序结束时,这块空间会自动还给操作系统;如果程序一直不结束,这块空间就一直不会被回收,就一直搁那,占据资源,浪费内存,此时我们称,造成了内存泄漏。
free函数会把ptr指针置成NULL吗?答案是:不会。free函数没有这个能力。如果你函数这个章节学的不错,你应该知道,C语言调用函数时是值传递,函数内部的形参是实参的一份临时拷贝,改变形参不会影响实参。也就是说,free函数内部会有另一个指针拷贝ptr的值,free函数会把这个指针指向的内存空间还给操作系统,但是没有能力影响外面的ptr指针。
既然ptr指针没有被置成NULL,也就是说,ptr的还是指向原来malloc申请的那块空间,但是这块空间已经还给操作系统了!如果还使用ptr指针访问这块空间,就造成了内存的非法访问,因为此时ptr是一个野指针!这是很危险的一件事。所以,在free掉这个指针之后,最好把它置为NULL,就像这样:
free(ptr); ptr = NULL;
最后还有一个细节,如果传给free函数的是NULL指针,free函数什么也不做。free(NULL);相当于什么也没发生。
来看看一段完整的代码,来演示malloc和free:
#include <stdio.h> #include <stdlib.h> int main() { // 开辟空间 int* ptr = (int*)malloc(10 * sizeof(int)); // 检验是否开辟成功 if (NULL == ptr) { perror("malloc"); return 1; } // 把1~10放到这块空间里 for (int i = 0; i < 10; i++) { ptr[i] = i + 1; } // 打印这块空间里的值 for (int i = 0; i < 10; i++) { printf("%d ", ptr[i]); } // 释放这块空间,别忘了把ptr置NULL free(ptr); ptr = NULL; return 0; }
输出结果:
3.3 calloc
calloc函数的使用和malloc函数几乎完全一样,只有2个微小的区别:
- calloc函数有2个参数,分别表示开辟空间要存储的元素个数和每个元素的大小。malloc函数只有一个参数,表示要开辟的空间的总大小。
- calloc函数会把开辟的空间初始化成0。malloc函数并不会做任何处理,所以空间内存储的是随机值。也就是说,calloc由于会对开辟的空间进行初始化,效率会低一点,相当于malloc+memset。
由于使用上就这点区别,大家看个例子就懂了。我只是把上面的代码改一下:
#include <stdio.h> #include <stdlib.h> int main() { // 开辟空间(40个字节,即10个int) int* ptr = (int*)calloc(10, sizeof(int)); // 检验是否开辟成功 if (NULL == ptr) { perror("calloc"); return 1; } // 打印这块空间里的值 for (int i = 0; i < 10; i++) { printf("%d ", ptr[i]); } // 释放这块空间,别忘了把ptr置NULL free(ptr); ptr = NULL; return 0; }
输出结果:
输出结果侧面验证了,calloc会把开辟的空间全部初始化成0。
3.4 realloc
刚刚讲了3个函数了,也只是把空间开辟出来,然后再还给操作系统,似乎也没有什么新鲜的?其实,最后一个函数,realloc,会让你大开眼界。
realloc函数可以帮你改变开辟的空间的大小,你可以让这块空间任意的变大变小,非常灵活!
realloc函数的声明如下:
void* realloc(void* ptr, size_t new_size);
第一个参数是你想扩容的空间的起始地址,也就是一开始接收malloc和calloc返回值的指针,第二个参数是你想把空间改变的新大小。注意:不是相对大小!是新大小!
比如,一开始先malloc出40个字节的空间,这块空间能存10个int:
int* ptr = (int*)malloc(40); if (NULL == ptr) { // 错误处理 // ... }
如果这块空间存满了,你还想再存10个int进去,此时新空间的大小就是20个int,即80个字节,比原空间多出40个字节。再次强调:realloc的第二个参数是新空间的总大小!是最终的大小,而不是相对的大小。所以如果把原来的40个字节的空间翻一倍,应该写realloc(ptr, 80);而不是realloc(ptr, 40);,后一种写法相当于空间大小没有改变。
realloc函数会返回新空间的起始地址,如果扩容失败就会返回NULL。那能不能这么写?
ptr = (int*)realloc(ptr, 80);
答案是:不行!你想想,如果扩容失败,返回NULL给ptr,相当于既没有扩容成功,还把原来的空间的地址给弄丢了,那不是赔了夫人又折兵吗?所以,还是那句话,得先验货,再使用。比如:
int* tmp = (int*)realloc(ptr, 80); if (tmp == NULL) { // 扩容失败 perror("realloc"); exit(-1); } else { // 扩容成功 ptr = tmp; }
那realloc函数具体是如何做到改变动态开辟空间的大小的呢?
realloc函数会先看一眼,原来空间后面的空间够不够。比如上面的例子中,realloc函数会去看,原来的空间再往后数40个字节的空间有没有被占用,如果没有,realloc会直接把原来空间的后40个字节的空间给申请到,再把原来空间的起始地址给返回,此时是原地扩容。
但是如果原来的空间的后面的空间被占用了呢?那就得另外找一块80个字节的空间,把原来的40个字节的数据拷贝过去,接着释放掉原来的空间,返回新空间的起始地址。这种扩容方式称为异地扩容。
还有一点,realloc函数也可以像malloc函数一样使用。当第一个参数是NULL时,realloc函数的表现和malloc一样。也就是说,malloc(40);和realloc(NULL, 40);等价。
我们把最初的程序改造一下,如下:
#include <stdio.h> #include <stdlib.h> int main() { // 开辟空间 int* ptr = (int*)malloc(10 * sizeof(int)); // 检验是否开辟成功 if (NULL == ptr) { perror("malloc"); return 1; } // 把1~10放到这块空间里 for (int i = 0; i < 10; i++) { ptr[i] = i + 1; } // 打印这块空间里的值 for (int i = 0; i < 10; i++) { printf("%d ", ptr[i]); } printf("\n"); // 扩容 int* tmp = (int*)realloc(ptr, 20 * sizeof(int)); if (NULL == tmp) { // 扩容失败 perror("realloc"); return 1; } else { // 扩容成功 ptr = tmp; } // 把11~20也放进去 for (int i = 10; i < 20; i++) { ptr[i] = i + 1; } // 打印这块空间的所有值 for (int i = 0; i < 20; i++) { printf("%d ", ptr[i]); } // 释放这块空间,别忘了把ptr置NULL free(ptr); ptr = NULL; return 0; }
输出结果如下:
总结
1.有时我们需要进行动态的内存管理,如果是静态内存管理,空间给少了不够用,给多了又浪费。动态内存管理是在堆区上进行的。
2.动态内存管理需要我们掌握4个函数,分别是malloc, free, calloc和realloc,它们分别扮演着重要的角色。
3.malloc和calloc负责开辟空间,并返回空间的起始地址。如果开辟空间失败会返回NULL。
4.malloc函数只有一个参数,表示开辟空间的总大小;calloc函数有两个参数,分别表示开辟空间能存储的元素个数和每个元素的大小。
5.malloc函数不会对空间进行处理,calloc函数会把空间初始化成全0。
6.free函数可以释放一块动态内存,传参时必须传这块空间的起始地址。释放完后最好置NULL,防止出现野指针。free(NULL)这种写法free函数什么都不会做。
7.realloc函数可以动态调整开辟空间的大小,传递参数时,第一个参数是动态内存的起始地址,第二个参数是新空间的总大小。如果第一个参数是NULL,realloc的表现和malloc一样。
8.使用malloc, calloc, realloc都需要判断返回值是不是NULL,并对相应情况进行处理。
到此这篇关于一文带你了解C语言中的动态内存管理函数的文章就介绍到这了,更多相关C语言动态内存管理函数内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!