1.为什么存在动态内存分配
1.1我们熟知的开辟内存方式有
在栈区上开辟4个字节的空间
int i = 0
在栈区上开辟16个字节的连续空间
int i[4] = { 0 };
1.2已知的开辟方式有以下两个特点
1. 空间开辟大小是固定的。
2. 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。
1.3原因
但是,有些时候,我们要通过运行程序才能知道数组需要的空间大小,亦或是在程序运行期间,需要增大数组的空间,这又该如何去做呢?
这就不得不引入动态内存开辟的概念了~
2. 动态内存函数的介绍
2.1 malloc
C语言提供了来自stdlib.h头文件中动态内存开辟的函数
void* malloc (size_t size);
此函数会向 堆区 申请一段连续可用空间,开辟成功则返回开辟空间的起始位置的地址
malloc函数的特点
开辟成功,返回这块空间的指针;
开辟失败,返回一个NULL指针,所以每一次开辟空间一定要检查;
返回类型为void* ,所以需要我们根据自己的需求,将返回的指针强制类型转化成我们需要的指针;
若size的单位是字节,若参数为0,malloc是未定义的,取决于编译器;
malloc函数具体使用
举个例子:开辟10个整形的空间,并分别赋值0~9的数字
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
printf("%s", strerror(errno));//若开辟失败,打印错误信息
return 1;//约定俗成:返回0表示正常,返回1表示不正常
}
int i = 0;
//赋值
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
//打印
for (i = 0; i < 10; i++)
{
printf("%d ",*(p + i));
}
return 0;
}
这时候可能有同学可能会问了:
那如果malloc函数开辟失败会怎么样呢?
演示一波~
在C语言中有这么一个定义好的值:INT_MAX
这是什么东西呢?
叫整形最大值,有多大呢?
转到定义来看看:
不得了,是一个21亿多的一个数字,这时候,让malloc函数开辟这么打一个空间试试?
嗯...好吧没想到电脑内存还蛮大的,开辟出来了...
这样,咱来个INT_MAX*INT_MAX
走~
还没运行,就已经提醒你了哈哈哈。
一个开辟空间失败的错误示范;
还有一个点:
什么是栈区,堆区?
内存里面分为三个区,就是栈区,堆区,静态区(实际上还有很多其他区,主要讲这几点),栈区是用来存放临时变量的,变量空间定义好就不能再变了,而malloc、calloc、realloc这几个函数就会在堆区上开辟空间,定义好后根据需求,可以继续改变变量所占空间的大小,静态区用来存放全局变量;(如下图)
细致一点就如下图:
有没有思考过这样一个问题?
即使一个空间用完了,退出程序,系统会自动回收空间, 但如果一个空间用完了,不想再用了,程序还在需要继续运行,继续开辟空间,那岂不是会造成内存泄露?
2.2 free
不用担心~😶🌫️
C语言专门提供了一个函数free,用来解决内存泄漏,回收空间的功能!原型如下:
void free (void* ptr);
先来看看free函数的特点吧~
1.如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
2.如果参数 ptr 是NULL指针,则函数什么事都不做。3.来源于stdlib.h
具体应用&&带入刚刚的例子
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
printf("%s", strerror(errno));//若开辟失败,打印错误信息
return 1;//约定俗成:返回0表示正常,返回1表示不正常
}
int i = 0;
//赋值
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
//打印
for (i = 0; i < 10; i++)
{
printf("%d ",*(p + i));
}
free(p);
p = NULL;
return 0;
}
这里又是一个细节~
p = NULL?
想象以下,释放了内存,可以不管这个指针p了吗?
经过调试可以看到:
free前:
注 意:p,10可以展示出10个元素,若只是p,只会显示一个值;
free后:
和free前有什么一样的,有什么不一样的?
free后,内存还给操作系统,动态开辟的空间里赋的值全部变为随机数,但是,p指针指向的地址依旧没变(x64环境)。
这样会使得指针p十分危险(野指针),即使空间还给了操作系统,但是p地址依旧不变,导致如果以后不小心误用了指针p会导致内存的非法访问!
所以一定要注意,每次释放完动态内存开辟的空间后,请将指针置为NULL;
2.2 calloc
C语言还提供了一个函数叫 calloc , calloc 函数也用来动态内存分配。
void* calloc (size_t num, size_t size);
calloc函数有什么特点呢?和malloc函数有什么区别呢?
calloc函数的特点是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0;
区别在于: malloc会将创建好的空间赋上随机值,而calloc 会在返回地址之前把申请的空间的每个字节初始化为全0;
举个例子:
可以从内存看出calloc函数开辟好空间,内存就已经全部初始化好成全零了
calloc貌似就等于malloc+memset(初始化全零)
所以如果在需要对开辟好的空间初始化成全零,calloc无疑是个很好的选择!
2.3 realloc
此函数的出现让动态内存开辟更加灵活!实现了真正意义上的 “动态”~
🤔写代码时,我们常常会遇到这样一个场景,开辟了一个40个字节的空间,但是后来实际操作中,又感觉不够用了,或者是感觉开辟大了,怎么办呢?
那 realloc 函数就可以做到对动态开辟内存大小的调整。
函数原型:
void* realloc (void* ptr, size_t size);
realloc函数的特点
1.ptr 是要调整的内存地址
2.size 调整之后新大小
3.返回值为调整之后的内存起始位置。
4.这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间
具体来讲realloc在调整内存空间的是存在两种情况:(如下图解)
例子:在malloc开辟的40个字节的空间扩容到80个字节的空间
情况一:
情况二:
由于上述的两种情况,realloc函数的使用就要注意一些。
举个例子:
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
else
{
//处理
}
p = (int*)realloc(p, 80);//注意这里!这样写可以吗?
if (p == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
else
{
//处理
}
free(p);
p = NULL;
return 0;
}
观察代码中的思考问题,这样写合理吗?
不合理!
想象以下,如果realloc空间开辟失败,就会传给p一个空指针,但是p本身是有指向malloc开辟的那块地址啊,一旦接收空指针,原来的那块空间不就废掉了吗?
所以可以考虑用个临时指针来接收,如果realloc开辟成功,再将临时指针赋值给p,就可以有效避免内存泄漏问题 ;
代码实现如下:
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
else
{
//处理
}
int* tmp_p = (int*)realloc(p, 80);
if (tmp_p != NULL)
{
p = tmp_p;
//处理
}
else
{
printf("%s\n", strerror(errno));
return 1;
}
free(p);
p = NULL;
return 0;
}
3. 常见的动态内存错误
对NULL指针的解引用操作
直接上代码:
int main()
{
int* ptr = (int*)malloc(INT_MAX * INT_MAX);
*ptr = 10;
return 0;
}
想象以下如果malloc开辟失败会发生什么?
malloc开辟失败返回一个NULL,赋值给ptr
造成对NULL指针的解引用操作!
对动态开辟空间的越界访问
看看以下代码哪里出问题了?
int main()
{
int* ptr = (int*)malloc(40);
if (ptr == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
//赋值
int i = 0;
for (i = 0; i <= 10; i++)
{
*(ptr + i) = i;
}
free(ptr);
ptr = NULL;
return 0;
}
很明显,当赋值10的时候发生了越界访问;
对非动态开辟内存使用free释放
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
int main()
{
int* ptr = (int*)malloc(40);
if (ptr == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
//赋值
int i = 0;
for (i = 0; i < 5; i++)
{
*ptr = i;
ptr++;
}
free(ptr);
ptr = NULL;
return 0;
}
这样是否可行?
ptr一旦++,导致ptr指针不在指向动态开辟的起始位置,这时候free会发生什么呢?(如下图)
这时候有人可能会说:那用const修饰以下不就很安全吗?
是啊,安全是安全了,但是ptr = NULL;不也不行了吗?从而产生野指针问题,所以一定要注意避免!
使用free释放一块动态开辟内存的一部分
void test()
{
int *p = (int *)malloc(100);
p++;
free(p);//p不再指向动态内存的起始位置
}
对同一块动态内存多次释放
int main()
{
int* ptr = (int*)malloc(40);
if(ptr == NULL)
{
return 1;
}
else
{
//处理
}
free(ptr);
//处理
free(ptr);//重复释放
return 0;
}
动态开辟内存忘记释放(内存泄漏)
int main()
{
int* ptr = (int*)malloc(40);
if(ptr == NULL)
{
return 1;
}
else
{
//处理
}
return 0;//忘记free
}
注 意:动态开辟的空间一定要释放,并且正确释放!