今天收假了,抓紧时间写几篇博客。我又来赶进度了。今天我们来讲解动态内存管理。🆗🆗
为什么存在动态内存分配
假设我们去实现一个通讯录,我们设置通讯录的大小是固定的100个元素,存放100个人的信息。如果信息太多,空间小了。如果信息太少,空间又大了。那我们应该怎样去解决?动态内存管理!
在目前为止,我们已经掌握两种向栈区申请内存的方式。
#include<stdio.h> int main() { int a = 10;//在栈空间申请四个字节存放一个值 int arr[] = { 1,2,3,4,5,6,7,8,9,10 };//在栈空间开辟连续的空间存放一组数 return 0; }
但是上诉的开辟空间的方式有两个特点:
- 空间开辟大小是固定的
- 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。
但是对于空间的需求,不仅仅是上诉的情况。有时候我们需要的空间大小在程序运行的时候才能知道那数组的编译时开辟空间的方式就不能满足了。 这时候C语言给程序员一种权利【能够动态的申请和管理内存空间】就是【动态内存开辟】,当然除了在申请的同时 我们也要学会释放空间。
当然我们头脑中还是要有【内存分布图】
动态内存函数的介绍
接下里我们分别给大家详细介绍一下动态内存开辟的函数 我们将从:头文件 函数参数 返回值 使用等方面去介绍。大家认真学起来!!
malloc
malloc - C++ Reference (cplusplus.com)
- 头文件 #include<stdlib.h>
- 函数参数 size_t size
- 函数参数表示开辟size个字节的空间大小,单位是字节
- 如果参数size为0,malloc的行为是标准是未定义的,取决于编译器。
- 函数返回值是void *
- 如果开辟成功,则返回一个指向开辟好空间的指针
- 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
- 返回值的类型是void* 是因为malloc函数并不知道开辟的空间类型,具体在使用的时候使用者自己来决定。
- 当malloc在使用的时候,已经知道是开辟的空间是存放那种类型的数据了,可以强制类型转化
- malloc函数是向内存申请一块连续可用的空间,并返回指向这块空间的指针。
- malloc函数和free函数是配合使用的。
- malloc函数申请的空间是需要释放的。
【malloc的使用需要注意:强制类型转化&&判断&&需要free释放】
//假设现在程序员A想申请40个字节的空间去存放10个整型 #include<stdio.h> #include<stdlib.h> #include<string.h> int main() { int* p = (int*)malloc(10 * sizeof(int));//强制类型转化 //开始存放 if (p == NULL) { perror("malloc");//为空的原因 return 1;//非0即不能正常返回 } int i = 0; for (i = 0; i < 10; i++) { p[i] = 1+i;//p相当于数组名 //p+i=i+1; } //打印 for (i = 0; i < 10; i++) { printf("%d ", p[i]);//p+i } //释放 free(p); p = NULL; return 0;//可以正常返回 }
忘记【perror库函数】戳一戳:C语言之字符函数&字符串函数篇(2)_唐唐思的博客-CSDN博客
当然如果申请的空间太大,也是不可以的!
#include<stdio.h> #include<stdlib.h> #include<limits.h> int main() { int* p = (int*)malloc(INT_MAX*4);//强制类型转化 //这里的空间过大会返回NULL的 if (p == NULL) { perror("malloc");//为空的原因 return 1;//非0即不能正常返回 } return 0; }
【malloc函数申请的空间是需要释放的】要怎么释放呢?
- 主动释放:配合free函数使用。
- 被动释放:程序退出之后,malloc函数申请的空间,就会被操作系统自动回收的。
- 注意:正常情况下,谁申请的空间,谁去释放。即便不释放,也要告诉别人,让别人有机会去释放。🆗🆗很重要!
free
free - C++ Reference (cplusplus.com)
- 头文件 #include<stdilb.h>
- free函数参数是 void*ptr
- 如果参数ptr指向的空间是动态开辟的,那么free会将其释放掉。
- 如果参数ptr 指向的空间不是动态开辟,那free函数的行为是未定义的。
- 如果参数ptr 是NULL指针,则函数什么事情也不做。
- 函数没有返回值
- free函数和malloc函数是配合使用的。
- free函数专门是用来做动态内存的释放和回收的。
- 特别提醒:free完了之后空间已经被释放了,p里面任然有地址,此刻p就变成了【野指针】,所以请把p赋值为NULL(空指针)。
free(p); p = NULL;//p是接收malloc开辟的空间的起始地址的指针变量
#include <stdio.h> int main() { //代码1 int num = 0; scanf("%d", &num); int arr[num] = { 0 }; //代码2 int* ptr = NULL; ptr = (int*)malloc(num * sizeof(int)); if (NULL != ptr)//判断ptr指针是否为空 { int i = 0; for (i = 0; i < num; i++) { *(ptr + i) = 0; } } free(ptr);//释放ptr所指向的动态内存 ptr = NULL;//是否有必要? return 0; }
calloc
calloc - C++ Reference (cplusplus.com)
- 头文件 #include<stdlib.h>
- 函数参数 size_t num size_t size (可以理解为将malloc的一个参数拆分为calloc的两个参数)
- 参数num是元素个数
- 参数size是一个元素的大小,单位是字节
- 函数返回值是 void*
- 如果开辟成功,则返回一个指向开辟好空间的指针
- 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
- 返回值的类型是void* 是因为calloc函数并不知道开辟的空间类型,具体在使用的时候使用者自己来决定。
- 当calloc在使用的时候,已经知道是开辟的空间是存放那种类型的数据了,可以强制类型转化
- calloc函数也是用来动态内存分配的
- calloc所申请的空间也需要free函数去释放
- 函数的功能为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。
- 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。
//向堆区申请10个整型的空间 calloc(10, sizeof(int)); malloc(10 * sizeof(int));
除了参数的区别,calloc函数申请好空间后【会将空间初始化为0】但是malloc函数不会初始化。
#include<stdio.h> #include<stdlib.h> int main() { int* p = (int*)calloc(10,sizeof(int));//强制类型转化 int* p = (int*)malloc(10*sizeof(int));//强制类型转化 //开始存放 if (p == NULL) { perror("malloc");//为空的原因 return 1;//非0即不能正常返回 } int i = 0; //打印 for (i = 0; i < 10; i++) { printf("%d\n", p[i]);//p+i } //释放 free(p); p = NULL; return 0;//可以正常返回 }
malloc打印出来的是随机值,而calloc打印出来是初始化为0的值。 根据需求使用,如果需要初始化为0,那我们可以使用【calloc】,如果不需要初始化为0,我们可以使用【malloc】
realloc
有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时候内存,我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小的调整。 realloc - C++ Reference (cplusplus.com)
- 头文件 #include<stdlib.h>
- 函数realloc的参数void* ptr,ptr是要调整的内存地址(内存的起始地址也就是原来malloc和calloc已经开辟的空间的起始地址p)
- ptr可以是NULL,ptr可以是空指针。
- 函数realloc的参数size_t size,size调整之后整体的新大小(调整新的整体的大小)单位字节
- 参数size不是指新增加或减少的差距❌ 是加上之前旧的大小,和新增的大小的整体计算
- 函数返回值是void *类型
- 返回值为调整之后的内存起始位置
- 返回值的类型是void* 是因为realloc函数并不知道开辟的空间类型,具体在使用的时候使用者 自己来决定。
- 如果开辟成功,则返回一个指向开辟好空间的指针
- 指向开辟好的空间的指针变量(分情况讨论)
1.可能与旧空间的起始地址一致 2.可能是一块全新的空间的起始地址
- 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
- realloc函数是对已经通过malloc和calloc函数开辟过的空间进行调整
- realloc函数的出现就是为了让动态内存管理更加灵活
- realloc函数在调整内存空间的是存在两种情况:
- 情况一:原有空间之后有足够大的空间。
- 情况二:原有空间之后没有足够大的空间。
- 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
关于realloc函数有返回值来接收有三种不同写法。
【写法1】
p = realloc(p, 20 * sizeof(int));//用旧空间的原来的指针变量去接收(NULL问题)
#include<stdio.h> #include<stdlib.h> int main() { int* p = (int*)calloc(10,sizeof(int));//强制类型转化 //开始存放 if (p == NULL) { perror("malloc");//为空的原因 return 1;//非0即不能正常返回 } int i = 0; //打印 for (i = 0; i < 10; i++) { printf("%d\n", p[i]); } //空间不够,希望调整空间为20个整型的空间 p = realloc(p, 20 * sizeof(int)); //不建议这样写 可能开辟空间失败返回NULL // 成功也就罢了,万一失败旧空间起始地址也找不到了 //释放 free(p); p = NULL; return 0;//可以正常返回 }
【写法2】
int* ptr = (int*)realloc(p, 20 * sizeof(int));//用新的指针变量去接收,但记住一定要释放
#include<stdio.h> #include<stdlib.h> int main() { int* p = (int*)calloc(10,sizeof(int));//强制类型转化 //开始存放 if (p == NULL) { perror("malloc");//为空的原因 return 1;//非0即不能正常返回 } int i = 0; //打印 for (i = 0; i < 10; i++) { printf("%d\n", p[i]); } //空间不够,希望调整空间为20个整型的空间 int *ptr = (int*)realloc(p, 20 * sizeof(int));//换一个指针变量去管理 //如果你就是使用ptr,一定记得要释放ptr所指向的空间 //释放 free(p); free(ptr); p = NULL; ptr=NULL; return 0;//可以正常返回 }
【写法3】
int* ptr = (int*)realloc(p, 20 * sizeof(int));
if (ptr != NULL)
{
p = ptr;
}
#include<stdio.h> #include<stdlib.h> int main() { int* p = (int*)calloc(10,sizeof(int));//强制类型转化 //开始存放 if (p == NULL) { perror("malloc");//为空的原因 return 1;//非0即不能正常返回 } int i = 0; //打印 for (i = 0; i < 10; i++) { printf("%d\n", p[i]); } //空间不够,希望调整空间为20个整型的空间 int *ptr = (int*)realloc(p, 20 * sizeof(int)); //但是,程序员还是想要p来管理这块空间,可以这么写 if (ptr != NULL) { p = ptr; } //释放 free(p); p = NULL; return 0;//可以正常返回 }
关于就是realloc函数返回的指针的两种不同的情况。
【情况1 】
要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。
- realloc函数此时的返回值是旧的空间的起始地址
【情况2】
原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址。 且realloc函数有三个特点
- realloc函数会将旧的空间的数据,拷贝到新的空间里
- realloc函数拷贝完成后,会将旧的空间释放掉
- realloc函数此刻的返回值不是原来的地址,而是新的空间的起始地址
关于原地和异地扩容的验证:
【原地扩容】
#include<stdio.h> #include<stdlib.h> int main() { int* p1 = (int*)malloc(40); int* p2 = (int*)realloc(p1,80);//扩容40 printf("p1=%p\n", p1); printf("p2=%p", p2); return 0; }
【异地扩容】
#include<stdio.h> #include<stdlib.h> int main() { int* p1 = (int*)malloc(40); int* p2 = (int*)realloc(p1,800);//扩容760 printf("p1=%p\n", p1); printf("p2=%p", p2); return 0; }
当然除此之外,realloc还可以当成malloc来使用,只要传空指针即可。
所以所以 ptr可以为空指针NULL
#include<stdio.h> #include<stdlib.h> int main() { int* ptr = (int*)realloc(NULL, 10 * sizeof(int)); if (ptr == NULL) { perror("realloc"); return 1; } free(ptr); ptr = NULL; return 0; }
常见的动态内存错误
- 对NULL指针的解引用操作
- 对动态开辟空间的越界访问
- 对非动态开辟内存使用free释放
- 使用free释放一块动态开辟内存的一部分
- 对同一块动态内存多次释放
- 动态开辟内存忘记释放(内存泄漏)
接下来我们一个一个纠错!
NO1.
//对NULL指针的解引用操作 void test() { int* p = (int*)malloc(40); //不做返回值的判断,就可能是使用空指针解引用 *p = 20;//如果p的值是NULL,就会有问题 free(p); }
【修改】加上返回值的判断
#include<stdio.h> void test() { int* p = (int*)malloc(40); if (p == NULL) { perror("malloc"); return 1; } *p = 20; free(p); } int main() { test(); return 0; }
NO2.
//对动态开辟空间的越界访问 #include<stdio.h> void test() { int i = 0; int* p = (int*)malloc(10 * sizeof(int)); if (NULL == p) { perror("malloc"); return 1; } for (i = 0; i <= 10; i++) { *(p + i) = i;//当i是10的时候越界访问 } free(p); p = NULL; } int main() { test(); return 0; }
【修改】不越界即可
#include<stdio.h> void test() { int i = 0; int* p = (int*)malloc(10 * sizeof(int)); if (NULL == p) { perror("malloc"); return 1; } for (i = 0; i < 10; i++) { *(p + i) = i;//当i是10的时候越界访问 } free(p); p = NULL; } int main() { test(); return 0; }
NO3.
//对非动态开辟内存使用free释放 #include<stdio.h> void test() { int a = 10; int* p = &a; free(p); p = NULL;//err } int main() { test(); return 0; }
【修改】不可以哈,删去free
//对非动态开辟内存使用free释放 #include<stdio.h> void test() { int a = 10; int* p = &a; } int main() { test(); return 0; }
NO4.
//使用free释放一块动态开辟内存的一部分 //示例1 #include<stdio.h> void test() { int* p = (int*)malloc(100); p++; free(p);//p不再指向动态内存的起始位置 } int main() { test(); return 0; } //示例2 #include<stdio.h> #include<stdlib.h> int main() { int* p = (int*)calloc(10, sizeof(int)); if (p == NULL) { perror("calloc"); return 1; } //赋值 int i = 0; for (i = 0; i < 5; i++) { *p = i; p++; } //0 1 2 3 4 0 0 0 0 0 for (i = 0; i < 5; i++) { printf("%d ", p[i]); } free(p);//只是释放了后面五个0 p = NULL; return 0; }
【修改】不要让p移动,想移动就另外设置一个指针让它去移动。养成好习惯不要动起始的指针
//示例1 #include<stdlib.h> #include<stdio.h> void test() { int* p = (int*)malloc(100); int* ps = NULL; if (p == NULL) { perror("malloc"); return 1; } else { ps = p; } ps++; free(p); p = NULL;//p不再指向动态内存的起始位置 } int main() { test(); return 0; } //示例2 #include<stdio.h> #include<stdlib.h> int main() { int* p = (int*)calloc(10, sizeof(int)); if (p == NULL) { perror("calloc"); return 1; } //赋值 int i = 0; for (i = 0; i < 5; i++) { p[i] = i; } //0 1 2 3 4 0 0 0 0 0 for (i = 0; i < 5; i++) { printf("%d ", p[i]); } free(p);//只是释放了后面五个0 p = NULL; return 0; }
NO5.
//对同一块动态内存多次释放 #include<stdlib.h> #include<stdio.h> void test() { int* p = (int*)malloc(100); free(p); free(p);//重复释放 } int main() { test(); return 0; }
【修改】不要多次释放
#include<stdlib.h> #include<stdio.h> void test() { int* p = (int*)malloc(100); free(p); } int main() { test(); return 0; }
#include<stdlib.h> #include<stdio.h> void test() { int* p = (int*)malloc(100); free(p); p = NULL; free(p);//ok } int main() { test(); return 0; }
NO6.
//动态开辟内存忘记释放(内存泄露) //示例1 #include<stdlib.h> #include<stdio.h> void test() { int* p = (int*)malloc(100); if (NULL != p) { *p = 20; } } int main() { test(); while (1);//忘记释放 } //示例2 #include<stdlib.h> #include<stdio.h> void test() { int* p = (int*)malloc(100); if (NULL != p)//这里直接跳出循环 { *p = 20; } free(p);//无用 p = NULL; } int main() { test();//调用完成 malloc申请的空间还在 //没有忘记释放。但是释放没有用 while (1);//死循环程序退出不了 }
【修改】 忘记释放或在函数内部释放了但是没有使用都会造成内存泄露
#include<stdlib.h> #include<stdio.h> int* test() { int* p = (int*)malloc(100); if (NULL != p) { *p = 20; } return p; } int main() { int *p=test(); free(p); p = NULL; while (1);//忘记释放 }
动态开辟的空间一定要正确释放!!
✔✔✔✔✔最后,感谢大家的阅读,若有错误和不足,欢迎指正!下篇博文我们讲解几道相关笔试题
代码------→【gitee:唐棣棣 (TSQXG) - Gitee.com】
联系------→【邮箱:2784139418@qq.com】