本章概述
- 为什么要有动态内存分配?
- malloc函数和free函数
- calloc函数和realloc函数
- 常见的动态内存的错误
- 彩蛋时刻!!!
为什么要有动态内存分配?
- 情况描述:当我们创建一个变量时,比如,
int i=0
;内存就直接分配一块空间,这个空间是固定的。再比如,我们创建一个数组,char arr[10]
;内存就直接分配了一块连续的空间,这个空间也是固定的。当我们要存放的数据过大时,原先申请的空间就不能满足了,就要重新创建变量,重新申请空间了,这样做很麻烦的。又或者,当我们的数据较小时,就会有用不到的空间,就会浪费掉。总之,对于空间的使用和申请不是很灵活,很不方便。 - 动态内存的介绍:上面咱们讲了内存申请的不灵活和不方便。动态内存就是解决这个问题的,因为有了动态内存,我们就可以想申请多少空间就可以申请多少的空间,甚至中间还能加或减少空间。这就使得我们申请内存空间很灵活了。关于内存,咱们之前讲过了,内存划分为栈区,堆区和静态区。函数参数,局部变量,结构体和数组这些都在栈区创建和开辟空间的。
malloc,free,calloc
和realloc
这些动态内存函数是在堆区。全局变量和常量这些是在静态区。所以,我们用动态内存函数是在堆区进行空间申请和创建的。 - 动态内存的缺点:动态内存对于内存的申请很灵活,这就导致了很容易出错。因为我们对于内存申请的权限变大了,对于我们管理内存的能力就要提高了,能力不够的话,很容易出错的。
- 注意:这些动态内存函数的头文件是
<stdlib.h>.
malloc函数和free函数
关于动态内存的学习,我们把malloc,free,calloc
和realloc
这几个内存函数掌握住就足够了。
- malloc函数:malloc函数是用来开辟内存的函数。我们先来看一下它的结构:
void* malloc (size_t size);
1.size_t size表示你要申请多少个字节的空间。
2.这个函数开辟的是一块连续的空间,和数组开辟的空间是一样的,都是连续的。
3.返回值是void*的指针,当这个函数开辟好空间后,就会返回这个空间的起始地址。
因为你要存的数据类型是不确定的,所以返回void*的指针。所以,当我们使用时,要强制类型转换。
- malloc函数的使用:我们开辟个4个int型的空间,进行代码展示:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int*)malloc(4*sizeof(int));
return 0;
}
结果运行图:
从结果运行图就可以看出来,开辟了一块连续的空间。我们就可以往这个空间里面存入一些数据了。前面,咱们讲过了,动态内存函数开辟的是一块连续的空间和数组开辟的方式一样。所以,我们往里面存值和取值的方式和数组是一样的。进行代码展示:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int*)malloc(4*sizeof(int));
int i = 0;
for (i = 0; i < 4; i++)
{
*(p + i) = i + 1; //存入数据1~4
}
for (i = 0; i < 4; i++)
{
printf("%d ", *(p + i));
}
return 0;
}
结果运行图:
我们还可以用数组的操作符' [ ]'
进行访问,进行代码展示:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int*)malloc(4 * sizeof(int));
int i = 0;
for (i = 0; i < 4; i++)
{
p[i] = i + 1; //存入数据1~4
}
for (i = 0; i < 4; i++)
{
printf("%d ",p[i]);
}
return 0;
}
结果运行图:
对于动态内存函数开辟的空间的使用,我们完全可以类比数组的使用方式。
- malloc函数使用注意事项:
- 1.如果开辟空间成功,就会返回这个空间的起始地址。
- 2.如果开辟空间失败,就会返回
NULL
,所以一定要做这个函数返回值的检查。 - 3.如果这个函数的参数为0,这个函数具体怎么做是标准未定义的,取决于编译器,在VS中,就什么也不做。
- free函数的介绍:
free
函数具有释放内存的作用。当我们创建的空间不再使用时,就要释放内存空间。这就好比,咱们从图书馆里面借书。如果你只借不换的话,图书馆里面的书总有一天就空了,而且你一直借着,别人也借不了。内存也是这个道理。 - free函数的使用:我们先来看这个函数的结构:
void free (void* ptr);
1.这个函数无返回值。
2.这个函数的参数是个指针类型,我们需要把我们开辟的空间起始地址传给free函数,才能释放空间
进行代码展示:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int*)malloc(4 * sizeof(int));
free(p);
p = NULL;
return 0;
}
结果运行图:创建的空间。
释放后空间:
- 使用注意事项:
- 1.
free
函数不可以释放栈区和静态区的变量空间,只能释放堆区(动态内存)的空间。进行代码展示:
- 1.
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
int main()
{
int i = 0;
free(&i);
return 0;
}
结果运行图:
- 2.
free
释放空间后,指针变量要立马赋予NULL,防止成为野指针(关于野指针的成因咱们讲过了)。当我们释放完空间后,虽然我们的指针变量里面还存放着那块内存的地址(未赋予NULL前)。但是,我们已经没有访问那块空间的权限了,free
之后就把这个关系给嘎断了。如图所示:
所以,我们一般都会申请内存函数和free函数一起使用。
calloc函数和realloc函数
- calloc函数的介绍:这个函数也是申请内存的函数,和
malloc
类似。但是,这个函数可以指定申请的空间数目和申请的空间大小。进行结构展示:
void* calloc (size_t num, size_t size);
这个函数表示你要申请多少num,空间大小为size的空间。
calloc
函数的使用:进行代码展示:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
int main()
{
int i = 0;
int* p = (int*)calloc(5,4); //申请5个空间大小为4个字节的空间
if (p ==NULL)
return 1;
for (i = 0; i < 5; i++)
{
p[i] = i + 1;
}
for (i = 0; i < 5; i++)
{
printf("%d ", p[i]);
}
free(p);
p = NULL;
return 0;
}
结果运行图:
malloc
函数和calloc
函数的区别:这俩的功能是一样的。但是,它们俩的唯一(主要)区别就是:malloc函数开辟好空间后,不会初始化。calloc函数开辟好空间后,会初始化。进行结果图的展示–这里只展示calloc的图,malloc的图上面展示过了:realloc
函数的介绍:这个函数的功能就是在原来开辟的空间上进行空间大小的更改。比如,当我们觉得空间小了,我们就可以在原来空间的基础上扩大空间。或者我们开辟的空间太大了,我们就可以在原来的基础上减少空间。进行结构展示:
void* realloc (void* ptr , size_t size) ;
1.ptr是原来空间的起始地址,因为我们是在原来的空间基础上更改空间的,所以要知道原来空间的地址
2.size表示我们要更改后的空间大小。
realloc
函数的使用:进行代码展示:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int*)calloc(5, 4); //申请5个空间大小为4个字节的空间
int* pp = (int*)realloc(p, 3*sizeof(int)); //把空间缩小到3个int型的空间大小
if (pp == NULL)
return 1;
else
p = pp;
free(p);
p = NULL;
return 0;
}
结果运行图:
- ```realloc```的返回值注意(限于扩大空间):关于它的返回值要有两点的注意:
- 1.当我们在原来空间的基础上开辟空间时,如果后面的空间足够时,就返回原来的空间地址。
- 2.当后面的空间不足时,realloc就会重新再找一块空间,进行新的空间开辟,然后返回新的空间地址。它先把原来的数据拷贝到新的空间里面,然后把原来的空间给释掉。如图所示:
- 3 .因为realloc函数有可能返回新的地址,但是空间开辟不足时就会返回NULL。如果,我们直接用同一个指针来接收返回值时,如果返回的是NULL,那么指针内容就会被更改,原来的空间也无法访问了。返回的不是NULL还好,就怕万一返回的是NULL。为了出现这种情况,我们要做个判断和中间变量。进行代码展示:
int* p = (int*)calloc(5, 4); //申请5个空间大小为4个字节的空间
int* pp = (int*)realloc(p, 3*sizeof(int)); //把空间缩小到3个int型的空间大小
if (pp == NULL)
return 1;
else
p = pp;
常见的动态内存的错误
- 1.对NULL指针的解引用操作,进行代码展示:
void test()
{
int *p = (int *)malloc(INT_MAX/4);
*p = 20; //如果p的值是NULL,就会有问题
free(p);
}
当我们开辟的空间过大时,就可能会返回NULL,一旦p被赋予NULL,我们就无法访问。在之间加个判断部分就OK了。
- 2 .对动态开辟空间的越界访问,进行代码展示:
void test()
{
int i = 0;
int *p = (int *)malloc(10*sizeof(int));
if(NULL == p)
{
exit(EXIT_FAILURE);
}
for(i=0; i<=10; i++)
{
*(p+i) = i;// 当i是10的时候越界访问
}
free(p);
}
当我们访问的空间超过了我们原本的空间大小,就会发生错误。
- 3.对非动态开辟内存使用free释放,进行代码展示:
void test()
{
int a = 10;
int *p = &a;
free(p); //ok?
}
咱们讲过了,free只能释放动态内存函数开辟的空间,其它情况都不能释放。
- 4.使用free释放一块动态开辟内存的一部分,进行代码展示:
void test()
{
int *p = (int *)malloc(100);
p++;
free(p); //p不再指向动态内存的起始位置
}
我们开辟多少的空间,就要释放多少的空间,所以要给起始地址。否则就报错。
- 5.对同一块动态内存多次释放,进行代码展示:
void test()
{
int *p = (int *)malloc(100);
free(p);
free(p); //重复释放
}
这个很好理解,我们已经释放过内存了,干嘛还要继续释放同一块空间呢,这不是纯做没用功吗。
- 6.动态开辟内存忘记释放(内存泄漏),进行代码展示:
void test()
{
int *p = (int *)malloc(100);
if(NULL != p)
{
*p = 20;
}
}
int main()
{
test();
while(1);
}
我们在使用完动态内存函数开辟的空间后,记住释放空间,也就是说动态内存函数和free是配套用的。
- 补充:
- 1.咱们说过,用完内存后就要释放。其实有时候,当我们忘记释放内存的时候,程序运行结束后,会自动释放内存的。但是,咱们还是要养成随用随释放的好习惯。
- 2.动态内存函数成功开辟空间后,会返回这个空间的起始地址,否则就返回NULL,所以要判断(检查)返回值。
彩蛋时刻!!!
https://www.bilibili.com/video/BV15K41147o7/?spm_id_from=333.788.recommend_more_video.0&vd_source=7d0d6d43e38f977d947fffdf92c1dfad
每章一句:我要对自己有耐心,因为我知道这是成长需要的。
感谢你能看到这里,点赞+关注+收藏+转发是对我最大的鼓励,咱们下期见!!!