很喜欢一部动漫,《星游记》,有这样一句话:“相信奇迹的人,本身和奇迹一样了不起!”
来看一下我们这次要抵达的彼岸吧!
一 . 为什么要有动态内存的分配。
二 . malloc和free的使用。
三 . calloc和realloc的使用。
四 . 动态内存常见的错误。
五 . 动态内存的经典试题分析。
正式开始之前我们先了解待会用到的内容!
1.动态内存开辟是在堆区进行操作!
2.malloc,calloc,realloc,所开辟的内存都是连续的!
3.动态内存开辟后释放的方式有哪些?
答:动态内存一但开辟好,释放方式就已注定,其一:程序结束后堆区开辟的内存由系统自动销毁! 其二:专门负责销毁堆区内存的库函数free来执行!
4.上面提到的销毁是什么?
答:需要使用内存的时候,我可以向操作系统的内存申请属于我的内存空间,当使用完后是要还给操作系统的,不能占为己有(操作系统说:你不用请归还,我还要给其他人用)这个就叫是销毁!所以销毁可不能理解字面意思将我们借到的内存给彻底删除呀!
进入正题!
一 . 为什么要有动态内存的分配
先看俩条语句
int val = 10;
int arr[10] = {0};
在没有学习动态内存管理前,开辟内存的方式有以上俩种,当程序运行后他们在栈区就完成了内存的开辟,并且无法做出对其大小的更改!这时就出现了内存管理上的问题 ,当使用静态数组写一个通讯录时,我们无法预测将来要存放信息量的大小,如果数组给大了则造成剩余空间的浪费,如果给小了则造成容量不足,所以C语言引入了动态内存开辟,可让使用者更高效更方便的对内存进行开辟和销毁 。
二 . malloc和free的使用。
1 . malloc函数的使用
void* malloc (size_t size);
通过malloc函数在堆区内存中开辟一块连续的空间,大小是size个字节,最后返回malloc出来的内存空间的指针,因为所开辟的内存空间不可能是负数所以用size_t类型。
注意:
(1)当malloc在堆区成功开辟好一块内存空间,则返回这块空间的指针。
(2)当malloc在堆区开辟空间失败后,则返回NULL指针,所以要对返回值进行检测 防止形成野指针。
(3)malloc函数的返回值类型是任意类型的指针,所以要根据自己的需求对函数返回 值进行处理,以便达到自己的需求。
(4)malloc函数开辟好的空间是不进行初始化的。
(5)给malloc函数的形参传入0,是一种错误写法,既然你想开辟内存但你又开辟0字 节这不是没事找事吗,这时malloc属于标准未定义行为。
图1:展示了上面的五点注意事项!这时还没有对动态内存进行释放!
图一
2 . free函数的使用
void free (void* ptr);
首先free函数是专门销毁动态开辟的内存,通过接收动态内存开辟的指针,来对其进行销毁!因为malloc,calloc,realloc的返回值都是任意类型的指针,要根据自己的需求对函数返回值进行处理,以便达到自己的需求,所以free函数的形参是任意类型的指针,以便接收任意类型的指针。
注意:
(1)free函数接收的指针必须是起始地址。
(2)free函数接收到NULL指针后,不做任何处理。
(3)free函数接收的指针必须是堆区的指针,若传入非堆区指针,则free标准为定义。
图2:有了free函数就可以对图1的代码进行完善,将有借有还体现出来!
图二
三 . calloc和realloc的使用。
1 . calloc函数的使用
void* calloc (size_t num, size_t size);
calloc函数在堆区开辟一块连续的内存空间,大小是num个size字节的内存空间,再将开辟好的内存空间的指针进行返回,这里值得留意的是,在返回前会将开辟好的内存空间初始化为0。
注意:
(1)malloc和calloc唯一的区别是:函数进行返回前calloc会将开辟好的内存空间初 始化为数值0。
(2)需要用到堆区内存初始化的情况就用calloc。下面演示calloc的使用。
int main()
{
int* p = (int*)calloc(10, sizeof(int));
if (p == NULL)
{
perror("calloc");
}
for (int i = 0; i < 10; i++)
{
printf("%d\n", *(p + i));//打印结果全是0,与malloc鲜明对比。
}
free(p);
p = NULL;
return 0;
}
2 . realloc函数的使用(非常重要)
void* realloc (void* ptr, size_t size);
realloc函数是将malloc,calloc,realloc在堆区开辟出来的内存进行调整,ptr指向的是被调整的地址,size是重新调整的大小,然后将调整后的内存的指针进行返回!
注意:
特殊情况:当ptr指向NULL时,realloc函数等价于malloc函数
(1)ptr指向的是被调整的对象,对象指的是malloc,calloc,realloc开辟的空间的 指针。
(2)size指的是重新开辟的大小(包括原有的大小)为什么会这样?
(3)返回调整后的指针。
(4)重要:realloc调整内存的俩种情况! 解答(2)中的疑问!
情况一:要调整的对象后面有足够的空间。
图三
情况二:要调整的对象后面没有足够的空间。
图四
(5)realloc不要使用接收calloc的指针变量,如果realloc正常返回那没问题,但如果 返回了NULL,则calloc开辟的内存就造成了内存泄漏。
以下代码将上面的5点注意都包含在内。
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p = (int*)calloc(10 , sizeof(int));
if (p == NULL)
{
perror("malloc");
return 1;
}
int* ptr = realloc(p, 20 * sizeof(int));
//防止造成内存泄漏,所以我先用ptr接收realloc的地址,然后判断ptr有没有问题,若没有,再给到p进行操作
if (ptr == NULL)
{
perror("realloc");
return 1;
}
else
{
p = ptr;
ptr = NULL;
//因为我要用p指针代替ptr,防止将free函数将p释放后,再用ptr找回去,形成野指针。
}
for (int i = 0; i < 20; i++)
{
printf("%d\n", p[i]);
}
free(p);
p = NULL;
//好的编程习惯是释放完记得将接收他地址的变量置为NULL。
return 0;
}
realloc函数的出现让我们对动态内存的管理更上一个台阶!假设用动态数组写一个通讯录,当容量等于元素个数时,就在堆区开辟原有内存的二倍,如果原有1000个元素满了而我又只需再添加一个那么剩余的999个内存是不是浪费了,那我们就要用到realloc函数来进行内存销毁,肯定有人会想,那我用free呀!那请问free能接收非起始地址的指针吗!!!再想,如果原有1000个元素满了而我又只需再添加一个,那我可以直接使用realloc再开辟一个内存即可,所以有了realloc后,我们可以通过realloc函数去调整堆区内存的大小。
四 . 动态内存常见的错误。
(1)对NULL指针解引用操作!
这里ULLONG_MAX超出了堆区的内存范围,malloc是无法开辟内存的所以返回了NULL,紧接着对NULL解引用就会造成野指针 (使用ULLONG_MAX)需要包含头文件 #include<limits.h>
int main()
{
int* p = (int*)malloc(ULLONG_MAX * sizeof(int));
if (p == NULL)
{
*p = 10;
}
//这里ULLONG_MAX超出了堆区的范围malloc是无法开辟内存的所以返回了NULL,
//紧接着对NULL解引用就会造成野指针。
return 0;
}
(2)对动态开辟的内存越界访问!
堆区只开辟了40字节,而我却要以整型的方式去访问第11个以及往后的整型,
就造成了堆区开辟内存的越界非法访问。
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL)
{
perror("malloc");
return 1;
}
for (int i = 0; i < 16; i++)
{
printf("%d", p[i]);
}
//堆区只开辟了40字节,而我却要以整型的方式去访问第11个以及往后的整型元素,
//就造成了堆区开辟的内存越界非法访问
return 0;
}
(3)对非动态开辟的内存使用free访问!
free是专门用来销毁动态开辟出来的内存,否则造成free标准未定义。
int main()
{
int a = 10;
int* p = &a;
free(p);
//free是专门用来销毁动态开辟出来的内存。
return 0;
}
(4)使用free释放一块动态开辟内存的一部分!
释放动态开辟内存只能从头释放!
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL)
{
perror("malloc");
return 1;
}
for (int i = 0; i < 3; i++)
{
p++;
}
free(p);
//释放动态开辟内存只能从头释放!
return 0;
}
(5)对同一块动态内存多次释放!
这里主要考察,free后一定要记住,置NULL接收动态内存地址的指针变量,这样就没有后面free的事了,因为给free传入NULL,free函数不做任何处理。
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL)
{
perror("malloc");
return 1;
}
free(p);
free(p);
//这里主要考察,free后一定要记住,置NULL接收动态内存地址的指针变量,这样就没有后面free的事了。
return 0;
}
(6)动态开辟内存无法释放造成内存泄漏!
realloc开辟失败返回NULL,导致calloc的地址丢失,自己占用,却自己不能用,同时别人还用不上,造成内存泄漏。
int main()
{
int* p = (int*)calloc(10 , sizeof(int));
if (p == NULL)
{
perror("malloc");
return 1;
}
int* p = realloc(p, ULLONG_MAX * sizeof(int));
//realloc开辟失败返回NULL,导致calloc的地址丢失,造成自己不能用的同时别人也不能用,造成内存泄漏
return 0;
}
(6)动态开辟内存忘记释放造成内存泄漏!
test函数调用完成后,p除了作用域被销毁,造成malloc的内存空间无法找到。
void test()
{
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL)
{
perror("malloc");
return 1;
}
else
{
*p = 66;
}
}
int main()
{
test();
//test函数调用完成后,p出了作用域被销毁,造成malloc的内存空间无法找到导致无法销毁,也可能是忘记销毁
return 0;
}
再次注意上面六条代码是错误警告!!!要避免的!!!
![](https://i-blog.csdnimg.cn/blog_migrate/9eec112e35474a9c97da35699bf5ce50.png)
不仅一番彻骨寒,怎得梅花扑鼻香。