C语言动态内存管理的实现示例
作者:米饭「」
一、什么是动态内存管理
动态内存管理是一种内存管理方法。它允许程序在运行时根据需要动态地申请和回收内存空间。与静态内存管理(如变量和数组的预先分配)不同,动态内存管理根据程序的需求即时分配内存,并且分配的内存大小就是程序要求的大小。
动态内存分配的优点:
- 能更好地适应系统的动态需求,提高内存的利用率
- 可以根据程序的实际需要分配内存,而不是预先分配固定大小的内存
- 允许程序在不需要时释放内存空间,供其他应用程序使用
总结来说,动态内存管理是一种灵活的内存管理策略,它允许程序在运行时根据实际的需求分配和释放内存,从而提高内存的使用效率和程序性能。
两种开辟内存空间的方式
我们现在已经掌握的内存开辟方式有两种:一种是创建变量,还有一种就是创建数组。
int i = 5 //在栈空间上开辟4个字节char arr[10] //在栈空间上开辟10个字节的连续空间
但是上述的两种开辟空间的方式有两个特点:
- 空间开辟的大小是固定的。
- 数组在申明的时候,必须指定数组的长度,数组空间一旦确定了大小就不能再调整但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,那数组编译时开辟空间的方式就不能满足了。
在C语言引入了动态内存开辟,让程序员自己动态的申请和释放空间,这样就比较灵活了。
二、动态内存管理的四个函数
要进行动态内存分配,就要掌握四个重要的函数:malloc、free、calloc、realloc。
1.malloc与free函数
C语言提供了一个动态内存开辟的函数:
void* malloc(size_t size);
这个函数会向内存申请一块连续可用的空间,并返回指向这块空间的指针(地址)。
♥ 如果开辟成功,则返回一个指向开辟好空间的指针(这块空间的起始地址)。
♥ 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
♥ 返回值的类型是void*,所以malloc函数并不知道开辟空间的类型,具体在使用的时候由使用者自己来决定。
♥ 如果参数size为0,那malloc的行为是标准未定义的,取决于编译器。
上面malloc函数中的参数是size_t类型的,也就是一个无符号整型。而malloc的参数size就表示需要申请多少个字节的空间。我们都知道,void*类型的指针是无具体类型的指针,可以用来接收任意类型的地址,但是不能对其进行解引用。那malloc函数的返回值,我们要怎么接收呢?很简单,对这个返回值进行强制类型转换即可。因为我们在动态的申请空间时,心里就已经知道自己需要多少个字节的空间,而且也知道申请的空间要用来存储什么类型的数据。比如我们要申请20个字节的空间用来存放5个整型:
#include<stdio.h> #include<stdlib.h> int main() { //申请20个字节的空间用来存放5个整型 int* p = (int*)malloc(20); //对指针p进行检查,看是否为NULL if (p == NULL) { perror("malloc"); return 1; } //使用空间 int i = 0; for (i = 0; i < 5; i++) { *(p + i) = i + 1; } //释放空间 free(p); p = NULL; return 0; }
在申请空间时要考虑到,有申请失败的时候,所以我们在动态的申请空间后,应该要对malloc的返回值进行检查。如果动态申请内存失败,那我们就不能再往下执行程序。上面代码中出现了另一个与malloc函数相匹配的free函数,free函数是专门用来释放动态申请的内存空间的。free函数的定义如下:
void free(void* ptr)
free函数要传进去的参数就是我们动态申请的内存空间的起始地址,记住传的参数一定得是这块空间的起始地址。有借有还嘛!我们动态地向操作系统申请空间来使用,那使用完了就要把申请的内存还回去。所以这里free函数就是⚾️专门用来释放和回收动态开辟的内存空间的⚾️。
但是对于free函数我们需要注意:
▲如果参数ptr指向的空间不是动态开辟的,那free函数的行为是未定义的
▲如果参数ptr是NULL指针,则函数什么事都不做
通过free函数释放了动态开辟的空间以后,不是就完了!我们用来接收malloc函数的指针p,还指向着原来申请空间的起始地址,但是这块空间已经被释放了。那以后要是不小又对指针p解引用来使用原来那块空间,就会造成非法访问,就会出现野指针。所以在释放了动态开辟的内存空间后,应该将指针p置为空(NULL),这样才是完整的。
malloc和free函数都声明在stdlib.h的头文件中,所以在使用这两个函数之前,要记得包含头文件stdlib.h。
由于动态开辟的内存空间是连续的,那你把这块空间看做数组也是可以的。因为数组在内存中就是连续存放的。
2.calloc函数
C语言还提供了一个函数叫calloc,calloc函数也是用来动态内存分配的。原型如下:
void* calloc(size_t num , size_t size)
♦ 函数的功能是为num个大小为size的元素开辟一块空间,并且把空间的每个字节初始化为0
♦ 与函数malloc的区别只在于calloc函数会在返回地址之前把申请的空间的每个字节都初始化为0比如我们现要为5个大小为4个字节的整型元素申请一块空间:
#include<stdio.h> #include<stdlib.h> int main() { //为5个大小为4字节的元素申请一块空间 int* p = (int*)calloc(5,sizeof(int)); if (p == NULL) { perror("malloc"); return 1; } //打印空间内容 int i = 0; for (i = 0; i < 5; i++) { printf("%d ", *(p + i)); } //释放空间 free(p); p = NULL; return 0; }
程序运行结果:
calloc函数除了在传参形式上与malloc有一点区别外,再一个就是它会将所申请空间的每个字节全部初始化为0,再返回此空间的起始地址。在上面的运行结果中,我们看到确实将此空间的每个字节初始化为了全0。我们也可以在内存中查看此空间的内容,如下图,确实初始化了空间内容为全0。
如果我们对申请的内存空间的内容要求初始化,那么可以很方便的使用calloc函数来完成任务。只是calloc函数在运行时间上可能就要比malloc函数慢,因为malloc函数不初始化申请的空间,所以两个函数各有各的好处。同样的,只要是动态开辟的内存空间,都要判断内存空间是否开辟成功!都要用free函数释放空间!都要把指针p置为空!养成良好的编程习惯,这有助于程序员少犯错误。
3.realloc函数
- realloc函数的出现让动态内存管理更加灵活。
- 有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的使用内存,我们一定会对内存的大小做灵活的调整。那realloc函数就可以做到对动态开辟内存大小进行调整。
realloc函数的定义形式如下:
void* realloc(void* ptr , size_t size)
▣ ptr是要调整的内存地址
▣ size是调整之后的新大小
▣ 返回值为调整之后的内存起始位置。
▣ 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
▣ realloc在调整内存空间时存在三种情况:
◎ 情况1:原有空间之后有足够大的空间。
◎ 情况2::原有空间之后没有足够大的空间
◎ 情况3:如果调整失败了,则realloc会返回NULL(空指针)
对于上面的第四第五条,如果我们一开始用malloc申请了20个字节的空间,现在要调整为40个字节的空间,那就有上面的三种情况,看下面的代码:
#include<stdio.h> #include<stdlib.h> int main() { //动态开辟20个字节的空间 int* ptr = (int*)malloc(5*sizeof(int)); if (ptr == NULL) { perror("malloc"); return 1; } //使用空间 int i = 0; for (i = 0; i < 5; i++) { *(ptr + i) = i + 1; } //调整原来空间的大小为40个字节 int* p = (int*)realloc(ptr, 10 * sizeof(int)); if (p != NULL)//调整成功 { ptr = p; int i = 0; for (i = 5; i < 10; i++)//继续使用空间 { *(ptr + i) = i + 1; } for (i = 0; i < 10; i++)//打印数据 { printf("%d ", *(p + i)); } //释放内存 free(ptr); ptr = NULL; } else//调整失败 { perror("realloc"); //释放内存 free(ptr); ptr = NULL; } return 0; }
程序运行结果:
如果使用realloc调整原来动态开辟的空间大小,那就有两种情况:
- 一种是上面的情况1,如果原来申请的空间后面有足够大的尚未使用的空间,那realloc就在原来申请空间的后面继续开辟20个字节空间,realloc返回的还是原来空间的起始地址。
- 另一种是上面的情况2,如果原来开辟空间后面没有足够的未使用的空间,那realloc函数就会在堆区上重新找一块连续的空间,这块空间是满足新的大小要求的,然后会将原来空间里的数据拷贝一份到新的空间里,然后再释放原来那块旧的空间,返回新的空间的起始地址。
- 最后一种就是情况3,realloc函数如果开辟空间失败了,那realloc函数会返回空指针。在上面的情况2中我们知道,如果realloc函数在堆区上重新找一块空间来满足现在的大小要求,那realloc函数会释放原来的空间,返回新空间的地址。那我们可不可以直接将指针p赋值给ptr,让指针ptr来维护这块新空间呢?答案是不可以的。我们要考虑到realloc函数会有申请空间失败的时候,要是realloc申请空间失败,我们直接将p赋值给ptr的话,那原来开辟的空间也就找不到了。所以在将指针p赋值给ptr时,应该要提前判断指针p是不是空指针,不是,再将指针p赋值给ptr。这样才是正确的。
记住只要是动态开辟的空间,都要用free函数释放申请的空间。
realloc还有一个特殊的用法,就是它可以完成和malloc一样的功能,比如我们要用malloc申请20个字节的空间,那用realloc也可以实现:
realloc(NULL , 20)
用realloc来开辟空间,只要记得传的第一个参数是NULL(空指针),第二个参数就是我们要开辟的空间大小(单位字节),那现在的realloc就等价于malloc。
三、常见的动态内存的错误
Ⅰ.对NULL指针的解引用操作
#include<stdio.h> #include<stdlib.h> int main() { int* p = (int*)malloc(5 * sizeof(int)); *p = 10; free(p); p = NULL; return 0; }
这里就没有对动态开辟的空间的指针(起始地址)进行检查,如果malloc申请空间失败,那malloc的返回值就是空指针。对NULL指针是不能对其进行解引用的。所以要对malloc的返回值做检查:
Ⅱ.对动态开辟空间的越界访问
对于动态申请的空间也是连续的,就像数组一样。上面为10个整型动态的申请了40个字节的空间。但是在使用空间的时候,上面存在了越界访问,我们申请来多少内存空间,那我们就只有这块空间的使用权限,不能越界使用空间。
Ⅲ.对非动态开辟内存使用free释放
#include<stdio.h> #include<stdlib.h> int main() { int a = 10; int* p = &a; free(p); return 0; }
我们说过只有动态开辟的空间,我们才会用free函数来释放,对于其他非动态开辟的内存空间,是不能用free来释放的,这是未定义的。
Ⅳ.free只释放了动态开辟内存的一部分
当我们用一个指针接收了动态开辟的内存空间的起始地址时,则对指针使用自加操作符,会改变它的指向位置,不再指向动态申请空间的起始位置,这时再将这个指针传给free函数,那free释放的就只是这块空间的一部分,并没有完全回收内存。所以对于动态开辟空间的指针(起始地址),要注意不要对其有任何的改动。
Ⅴ.对同一块动态内存多次释放
我们在写代码时,对于动态开辟的内存,我们可能会重复的释放,这也是不行的。像这种错误,可能在我们写大量的代码时会遇到,也就是你知道前面你动态开辟内存了,但是由于写了很多行的代码,可能就会忘记自己已经对动态开辟的内存释放过了,你又来一次释放。这也是动态开辟内存会遇到的错误。所以一定要检查用了多少次动态内存开辟,对应的就要有多少次回收释放。
Ⅵ.动态开辟内存忘记释放(内存泄漏)
忘记释放不再使用的动态开辟的空间会造成内存泄漏,你是向操作系统申请空间来用,但是你用了却没有把这块空间还给操作系统,那这块内存就可能还存储着一些值,这块空间就用不了,一直占着内存,造成内存泄漏。当然,如果你忘记了使用free函数释放动态开辟的空间,在程序运行结束的时候,操作系统也会自动回收这块内存。所以本质上动态开辟的空间是不会丢的,但是如果程序一直运行着,那就可能会造成内存泄漏。
切记:动态开辟的空间一定要释放,并且正确释放
到此这篇关于C语言动态内存管理的实现示例的文章就介绍到这了,更多相关C语言动态内存管理内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!