一、动态内存管理是什么?
如果我们需要创建一个变量,可以直接通过类型名+变量名创建即可.此时会自动向内存申请该类型所需要的的字节空间,例如:int a=0;
该语句会自动向内存申请四个字节的空间(64位机器下),那么如果我们需要多个变量呢?
很显然,在之前,我们就学过数组,数组可以解决创建多个变量的问题,但是,即使是数组也存在一个缺陷.
那就是在创建数组时,我们必须要先确定数组的大小,这样操作系统才会去向内存申请固定大小的字节空间.
而在很多情况下,我们并不能确定要存储的变量个数,这是很常见的问题,
🌰例如:
外卖平台并不能提前知道今天的订单量,淘宝商家也一样不能预测今天商品的销售量等等.
包括我们之前讲解的通讯录简易版我们并不能事先知道要添加的联系人个数,此时用数组去存储,很难确定开多大的数组,开大了浪费,开小了不够用.
为了解决这个尴尬的问题,c语言提供了一些可以申请内存空间的函数,这些函数被称为动态内存函数.malloc函数,calloc函数以及realloc函数.
二、内存操作函数
2.1 malloc函数与free函数
malloc
函数原型:
参数介绍:
参数 | 意义 |
size | 要申请的字节个数(记住单位是字节) |
函数作用:
malloc函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
如果申请成功:则返回一个指向开辟好空间的指针。
如果申请失败:则返回一个NULL指针,所以我们在使用malloc函数申请空间时,要判断返回值是否为空,空指针则代表申请失败。当然这种情况是很少发生的,但是作为一名合格的程序员,还是建议加上返回值的判断,这也是对程序员自己的帮助.
返回值解释:
返回值的类型是 void* ,因为我们在使用malloc函数申请空间时可以给多种类型赋值,不能限制返回值的类型,在具体使用时,强制转换为需要的类型即可.
示例:
#include <stdio.h> #include <stdlib.h>//malloc函数所需要的头文件 int main() { //向内存申请10个整形所占的字节个数的空间,通过强转为int*后赋值给a int* a = (int*)malloc(10 * sizeof(int)); if (a == NULL) { perror("malloc a fail");//申请失败时,打印错误信息 return 0; } //向内存申请2个双精度形所占的字节个数的空间,通过强转为int*后赋值给b double* b = (int*)malloc(2 * sizeof(double)); if (b == NULL) { perror("malloc b fail");//申请失败时,打印错误信息 return 0; } return 0; }
例图:
还有人很调皮,将size设置为0,malloc(0);这就让编译器很无奈,这种行为是未定义的,0就是不申请空间吗?
不申请你找我(malloc)干嘛?咱还是规规矩矩的写代码,做一个乖孩子吧.
free
我们在介绍free函数之前,先简单介绍一下内存部分分区吧!
栈区:
用于存放局部变量,函数参数等临时变量.
堆区:(今天的重点)
是用于供程序员申请的内存区,malloc函数,calloc函数和realloc函数就是在这里申请内存空间.
静态区:
用于存放全局变量和静态变量.
当我们在自定义一个函数时,会在栈区上开辟一块空间给该函数,当函数调用结束,为函数开辟的空间就会被收回,则其中的变量也会被销毁.但是malloc函数申请的空间不会,因为它是在堆区上申请的空间,需要申请者自己去释放,而这项操作就需要使用函数free.
函数模型:
free函数只用来释放动态开辟的内存即用malloc、calloc以及realloc开辟的空间。
参数说明:
如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。同样还是那句话,不和规则的事咱就不要做了.
如果参数 ptr 是NULL指针,则该函数不会进行任何操作.
说了这么多,我们实践操作一下吧!
#include <stdio.h> #include <stdlib.h>//malloc函数所需要的头文件 int main() { int* a = (int*)malloc(10 * sizeof(int));//向内存申请10个整形所占的字节个数的空间,通过强转为int*后赋值给a if (a == NULL) { perror("malloc is fail");//申请失败时,打印错误信息 return 0; } //赋值 for (int i = 0; i < 10; i++) { a[i] = i; } //打印 for (int i = 0; i < 10; i++) { printf("%d ", a[i]); } free(a); a = NULL; return 0; }
运行结果:
0 1 2 3 4 5 6 7 8 9
在为赋值之前,我们观察一下a空间中存放的值,明显是一些未初始化而产生的的"随机值".
赋值前:
赋值后:
注意:
free(a)后,a指针所指向的内存空间就被释放掉了,后续就不能使用了,则应当为了防止出现空指针,则需要进行"置空"操作.a = NULL;
2.2 calloc函数
函数模型:
参数说明:
num:要申请的元素个数.
size:一个元素所占的内存大小.
将上面的malloc代码改成calloc函数后:
#include <stdio.h> #include <stdlib.h>//malloc函数所需要的头文件 int main() { int* a = (int*)calloc(10 , sizeof(int));//向内存申请10个整形所占的字节个数的空间,通过强转为int*后赋值给a if (a == NULL) { perror("calloc is fail");//申请失败时,打印错误信息 return 0; } //赋值 for (int i = 0; i < 10; i++) { a[i] = i; } //打印 for (int i = 0; i < 10; i++) { printf("%d ", a[i]); } free(a); a = NULL; return 0; }
该函数的重点是能理解与malloc函数区别.
赋值前:
赋值后:
malloc与calloc的区别:
很明显,相比于malloc函数,在申请空间成功后,calloc函数会将申请到的空间全部初始化为0.这里可能有人要问,为啥我们不直接用calloc函数,还需要malloc函数干嘛?
原因是malloc是只需要将空间申请下来就行,而calloc函数还需要清理空间(都初始化为0),这样calloc函数的执行效率就没有malloc快.在很多情况下,我们并不需要初始化为0,这时候直接使用mallo函数就行,效率会高一些.
总结:
malloc | calloc |
申请到的空间未被初始化 | 申请到的空间全部被初始化为0. |
执行效率相对较高 | 执行效率相对较低 |
2.3 realloc函数
函数原型:
参数说明:
参数 | 意义 |
ptr | 需要重新分配内存空间的地址 |
size | 重新分配后内存空间的大小 |
函数功能:
realloc函数就是为了使得动态内存函数更加配得上"动态"之词的函数.
回到之前的问题,有时会我们发现过去申请的空间太小了,有时候我们又会发现申请的空间过大了导致内存浪费,那为了合理的申请内存.我们需要对内存的大小做灵活的调整。
realloc函数就是重新分配之前开辟的空间大小.
返回值:
返回值为调整之后的内存起始位置。
很重要!!!
这时有两种情况:
①:原地扩容:
原地址后面有足够的空间支持扩容.这时,会占用后面未被分配的内存空间用于扩容.
②:异地扩容:
原地址后面的内存空间不够支持扩容,则需要找到另外一块内存空间,将数据拷贝过去,然后再扩容.返回新的地址.
图解:
三、动态内存函数操作不当造成的错误:
(1)访问空指针
对申请的空间忘记进行NULL指针判断,导致访问空指针
这里一次申请大量的内存空间,内存没有那么多,会申请失败,返回NULL指针.
#include <stdio.h> #include <stdlib.h>//malloc函数所需要的头文件 int main() { int* a = (int*)malloc(100000000000000*sizeof(int)); //赋值 for (int i = 0; i < 10; i++) { a[i] = i; } //打印 for (int i = 0; i < 10; i++) { printf("%d ", a[i]); } free(a); a = NULL; return 0; }
运行结果:
(2)对同一块空间就行多次释放:
#include <stdio.h> #include <stdlib.h>//malloc函数所需要的头文件 int main() { int* a = (int*)calloc(10, sizeof(int));//向内存申请10个整形所占的字节个数的空间,通过强转为int*后赋值给a if (a == NULL) { perror("malloc a fail");//申请失败时,打印错误信息 return 0; } free(a); //中间含有大量代码 free(a);//导致忘记已经释放过了 a = NULL; return 0; }
(3)向释放申请空间的一部分:
申请的空间不能释放其中的一部分,只能一次全部释放.
#include <stdio.h> #include <stdlib.h>//malloc函数所需要的头文件 int main() { int* a = (int*)calloc(10, sizeof(int));//向内存申请10个整形所占的字节个数的空间,通过强转为int*后赋值给a if (a == NULL) { perror("malloc a fail");//申请失败时,打印错误信息 return 0; } int* b = a + 3; free(b); b = NULL; return 0; }
(4)内存泄漏(重点):
忘记释放在堆区上释放的空间
#include <stdio.h> #include <stdlib.h>//malloc函数所需要的头文件 int main() { int* a = (int*)calloc(10, sizeof(int));//向内存申请10个整形所占的字节个数的空间,通过强转为int*后赋值给a if (a == NULL) { perror("malloc a fail");//申请失败时,打印错误信息 return 0; } //赋值 for (int i = 0; i < 10; i++) { a[i] = i; } //打印 for (int i = 0; i < 10; i++) { printf("%d ", a[i]); } return 0; }
此时,程序并不会报错,但是这时会出现一个很严重的问题,那就是内存泄漏,用malloc函数申请的空间并没有被释放,导致一直占用内存空间.
当然,在程序结束时,系统会自动回收这些未被释放的空间,但是对于一些大型的程序或者在特定情况下,这是非常可怕的.
例如:
1)如果内存泄漏发生在手机上,一次泄漏一点点,手机长期不关机,几天或者几个星期之后,运行内存都被挤满了,会导致手机特别卡.
2)大型服务器是开机后,除了维修或者老化被替代,都是一直不关机的,此时内存泄漏是很可怕的,造成的损失也特别严重.
四、柔性数组与变长数组.
什么是柔性数组?
可能有人在此之前并没有听过柔性数组这个词.
柔性数组表示,在进行定义结构体类型时,结构体的最后一个成员可以是一个不指定大小的数组,这个数组就被称为柔性数组.
例如:
typedef struct test { char name[10]; int data[];//柔性数组 //也可以写成int data[0]; }test_struct;
柔性数组的规则:
1.柔性数组前面至少要有一个成员变量,且柔性数组是最后一个成员.
2.在用sizeof对结构体进行计算时,不会计算柔性数组的大小.
3.柔性数组不能直接使用,需要malloc函数进行分配时分配,且分配的大小必须比不计算柔性数组所占的空间要大,要给柔性数组预留空间.
#include <stdio.h> #include <stdlib.h> //定义一个包含柔性数组的结构体 typedef struct test { char name[10]; int data[]; //也可以写成int data[0]; }test_struct; int main() { //创建一个结构体指针,并未柔性数组分配空间 test_struct* test1 = (test_struct*)malloc(sizeof(test_struct)+ 10 * sizeof(int)); //sizeof(test_struct)表示不计算柔性数组时结构体所占内存大小. //10 * sizeof(int)表示在此基础上再增加10个整形的字节空间,会分配给柔性数组. //初始化柔性数组 for (int i = 0; i < 10; i++) { test1->data[i] = i; } //打印 for (int i = 0; i < 10; i++) { printf("%d ", test1->data[i]); } return 0; }
柔性数组的优点:
1.由于是连续的内存空间,所以释放时可以一次性释放,不需要分两次释放.
2.同样是因为连续的空间,访问速度较于不连续空间速度更快,因为寄存器一次性读取数据是按连续内存读取的,不连续则需要读取多次.
变长数组:
在c99标准中支持可以用变量来定义数组的大小.
即:
int main() { int a = 10; int arr[a]; return 0; }