【C语言进阶】之动态内存管理
- 1.为什么我们需要动态内存管理
- 2.动态内存管理的函数介绍
- 2.1malloc函数和free函数
- 2.1.1malloc函数
- 2.1.2 free函数
- 2.2calloc函数
- 2.3realloc函数
- 3.动态内存管理中经常出现的一些问题总结。
- 3.1 越界访问
- 3.2 对空指针进行解引用操作
- 3.3 对同一片空间进行多次释放
- 3.4 释放非动态开辟的空间进行释放
- 3.5 忘记释放动态开辟的空间
- 3.6 野指针问题
- 3.7只释放一部分动态开辟的空间
📃博客主页: 小镇敲码人
🚀 欢迎关注:👍点赞 👂🏽留言 😍收藏
🌏 任尔江湖满血骨,我自踏雪寻梅香。 万千浮云遮碧月,独傲天下百坚强。 男儿应有龙腾志,盖世一意转洪荒。 莫使此生无痕度,终归人间一捧黄。🍎🍎🍎
❤️ 什么?你问我答案,少年你看,下一个十年又来了 💞 💞 💞
1.为什么我们需要动态内存管理
我们经常开辟内存有如下两种方法:
#include<stdio.h>
int main()
{
int a = 0;
int b[20] = { 0 };
return 0;
}
这两种开辟内存的方法都有如下特点:
- 空间是固定的。
- 开在栈区。
- 数组需要指定大小。
但是我们在C语言刷题时,可能会遇见数组的大小在程序运行中输入了才知道的情况,如果直接开一个比较大的数组就很浪费空间,这个时候就需要用到动态内存管理。
另外,为什么要提到它是开在栈上的空间呢?因为栈上面开的空间它有一个特点,函数生命周期结束,它里面开的临时变量和固定大小的数组的内存系统也就回收了,我们如果想在一个非main函数里面开一块空间,要达到这个函数结束我的空间还在,没有被系统回收的目的,就需要动态内存管理函数的使用,因为其是在堆上开的空间,在堆上开的空间有一个特点,除非你手动释放,或者main函数结束,否则你的系统是不会回收这片空间的。
2.动态内存管理的函数介绍
内存管理函数有一个共同的头文件,stdlib.h
。
2.1malloc函数和free函数
2.1.1malloc函数
C语言提供了一个叫做malloc
的函数,它的函数定义是这样的:
void* malloc(size_t size) ;
因为编译器不知道你要在堆上开辟哪个类型的空间,所以它的返回值就设为万能指针void *
。
因为开辟内存肯定返回值是一个地址,但是可以不指明地址的类型,我们在指针进阶篇谈到过,使用void*
指针前是必须强制类型转换为我们需要的类型,这个是程序员自己控制的。
至于这个函数的一个参数,自然是你想开辟内存的大小,单位是字节,有人可能想问,如果这个参数传0会发生什么呢?这个是标准未定义行为,不同编译器不同。
如果开辟内存成功,就会返回一个void *
地址的地址,如果开辟失败就会返会NULL
,它不会给空间初始化一个值。
下面我们来演示一下这个函数的使用:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main()
{
int n = 0;
scanf("%d", &n);
int* a = (int*)malloc(sizeof(int) * n);
if (a == NULL)
{
perror("malloc failed");
exit(-1);
}
memset(a, 0, sizeof(int) * n);
for (int i = 0; i < n; i++)
{
printf("%d ", a[i]);
}
free(a);
a = NULL;
return 0;
}
最后三行我们先不管,至于中间判断开辟内存失败的代码,如果你不知道,可以去看一下博主这篇文章【数据结构初阶】之单链表。
我们来看看运行结果:
另外我们也可以测试一下什么时候会malloc
失败:
可以看到当我们在堆上开3000 000 00*4个字节的空间时,会malloc失败。
因为1B就是1字节,1KB = 1024B,1MB = 1024KB,1G = 1024MB,我们算了一下大概是开286MB左右的空间,malloc才会失败,所以我们平时写代码可以不加这个判断,但是在大的工程项目中加上可以增加我们代码的健壮性。
至于如果传的大小是0会怎么样,我们可以看一下VS2019是如何处理的:
可以看到程序是正常退出了的。
- 注意,有时候我们把下面的
a
又叫做动态数组。
int* a = (int*)malloc(sizeof(int) * 6);
因为a
的空间是连续,而且可以变化,不是固定的,能多次更改,而且可以通过[]
操作符访问,我们把这个a
又叫做动态数组。
2.1.2 free函数
free
函数是用来手动释放动态开辟空间的函数,它的声明是这样的:
void free (void* ptr);
我们只需要传一个保存了动态数组首元素地址的那个指针变量就可以回收那片空间。
2.2calloc函数
C语言还提供了一个动态内存管理的函数,叫做
calloc
,它的函数声明是这样的:
void* realloc(size_t num,size_t size);
- 函数的功能是为开辟
num
个大小为size的元素开辟一片空间,并给它们的每个字节初始化为0,它和malloc
函数的区别就在于,malloc
函数不会初始化。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main()
{
int n = 0;
scanf("%d", &n);
int* a = (int*)calloc(n,sizeof(int));
if (a == NULL)
{
perror("calloc failed");
exit(-1);
}
for (int i = 0; i < n; i++)
{
printf("%d ", a[i]);
}
free(a);
a = NULL;
return 0;
}
a的内存调试结果:
可以看到,程序执行到光标位置,a的内存每一个字节的内容已经全部被初始化为0了。
2.3realloc函数
realloc
函数也是C语言给我们提供的一个函数,有时候我们malloc一片空间后,发现不够用了,就需要使用realloc
给那个空间扩容。
void* realloc (void* ptr, size_t size);
它的第一个参数是一个指针变量,第二个参数size是调整之后的新大小。
我们通常会出现如下两种情况:
- 有一片size大小的连续的空间,返回的地址还是原先的指针变量的地址。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main()
{
int n = 0;
scanf("%d", &n);
int* a = (int*)malloc(n * sizeof(int));
if (a == NULL)
{
perror("malloc failed");
exit(-1);
}
int* tmp = (int*)realloc(a, (n + 1) * sizeof(int));
printf("%p %p", a, tmp);
free(a);
a = NULL;
return 0;
}
这里我们只扩容了4个字节的空间应该是不用重新找一片连续的空间的,我们看运行截图:
我们可以看到返回的地址确实和未扩容前a的地址是一样的。
2.没有一片连续的size大小的空间,如果开辟空间成功,realloc会将之前的数据拷贝到一片新的连续的空间中,并帮助你把原先旧的空间给释放掉。
如果你不相信,我们可以通过下面的代码来验证一下:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main()
{
int n = 0;
scanf("%d", &n);
int* a = (int*)malloc(n * sizeof(int));
if (a == NULL)
{
perror("malloc failed");
exit(-1);
}
int* tmp = (int*)realloc(a, (n + 500) * sizeof(int));
if (tmp == NULL)
{
perror("realloc failed");
exit(-1);
}
printf("%p %p",a,tmp);
free(tmp);
tmp = NULL;
return 0;
}
运行截图:
此时a的地址已经和tmp的不相同了,因为我们在原先的堆区的位置,找不到一片连续的510字节的空间,a的地址那片空间已经释放过了,如果你再释放a,系统就会报错:
如果扩容失败就会返回NULL
指针:
所以我们在使用realloc
指针时应该先用tmp来保存其返回的地址,因为如果扩容失败,返回NULL
直接把NULL
赋值给a,那我们a的数据就找不到了,所以先赋值给tmp,并加上判断,是为了数据的安全考虑,正确的使用方法是这样:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main()
{
int n = 0;
scanf("%d", &n);
int* a = (int*)malloc(n * sizeof(int));
if (a == NULL)
{
perror("malloc failed");
exit(-1);
}
int* tmp = (int*)realloc(a, (n + 10) * sizeof(int));
if (tmp == NULL)
{
perror("realloc failed");
exit(-1);
}
a = tmp;
memset(a, 0, (n + 10) * sizeof(int));
for (int i = 0; i < n + 10; i++)
{
printf("%d ", *(a + i));
}
free(a);
a = NULL;
return 0;
}
realloc
也不会给它开辟的地址空间初始化,而且当第一个参数传NULL
时,它的功能就相当于malloc
函数,
我们可以简单的使用一下:
3.动态内存管理中经常出现的一些问题总结。
3.1 越界访问
越界访问就是对不属于你的空间进行操作,在进行free操作的时候会报错,请看如下代码:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main()
{
int* a = (int*)malloc(sizeof(int) * 10);
if (a == NULL)
{
perror("malloc failed");
exit(-1);
}
for (int i = 0; i <= 10; i++)
{
*(a + i) += 1;
}
free(a);
a = NULL;
return 0;
}
报错截图:
这里正常应该没有等于,因为我们只开了10个int
型的空间,有了等于就非法访问了后面一个不属于我们的四个字节的空间,free
时会报错。
如果只是遍历一下,打印一下那里面的值,编译器似乎是检查不出来的,
这种情况编译器虽然不报错,但还是比较危险,严格意义上也属于越界访问,不要去做。
3.2 对空指针进行解引用操作
NULL
是不能进行解引用操作的,我们在使用动态内存函数时可能会出现这种情况:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main()
{
int* a = (int*)malloc(sizeof(int) * INT_MAX);
*a = 4;
return 0;
}
运行截图:
这里我们如果加上一个a == NULL
的判断,我们就可以知道问题了,也不会出现对空指针进行解引用的未定义操作,程序就不会异常挂掉了。
3.3 对同一片空间进行多次释放
我们不能多次释放我们已经释放过的空间。
否则编译器会强制的报错。
3.4 释放非动态开辟的空间进行释放
free
只能释放动态开辟的空间,不能释放临时变量的空间。
这里我们释放掉n
的空间,程序崩溃了,因为n
是临时变量。
3.5 忘记释放动态开辟的空间
这里有人就要问了?为什么要手动释放堆上开的空间呢?程序运行结束之后,系统不是自动回收吗,我们这样做不是多次一举吗?
堆上开的空间想释放只有两种办法:
- free函数手动释放。
- main函数结束,程序运行结束,系统自动回收。
注意:有些程序是永远都在运行着的,比如我们手机上的淘宝,你一打开它就一直运行,很多空间都是堆上开的,一个函数可能会重复执行很多次,如果你使用了堆上的空间不主动释放,程序也还没结束,就会造成内存泄漏,久而久之内存被占完了,程序就会挂掉。
3.6 野指针问题
还有一点,为什么释放那片空间后,还要把相应的指针变量赋值为空呢?因为那片空间已经不属于我们了,被系统回收了,是野指针,为了防止你非法访问造成一些我们很难查出的问题,赋为空值是最好选择。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main()
{
int* a = (int*)malloc(sizeof(int) * 10);
if (a == NULL)
{
perror("malloc failed");
exit(-1);
}
printf("%p\n", a);
memset(a, 0, sizeof(int) * 10);
free(a);
*a = 4;
printf("%p\n%d", a, *a);
return 0;
}
运行截图:
可以看到,程序运行是正常的,但是a的空间被系统回收后,a仍然保存的还是那片空间的地址,但那片空间已经被系统回收了,它就是一个野指针了。
我们访问那个地址是非法的,但是编译器检查不出来,如果我们养成好习惯,在释放空间后主动将a赋为NULL
就不会出现检查不出来的问题了,因为对NULL
解引用程序会崩溃。
3.7只释放一部分动态开辟的空间
我们也不能只释放a的一部分空间,这是编译器不允许的行为:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main()
{
int* a = (int*)malloc(sizeof(int) * 10);
if (a == NULL)
{
perror("malloc failed");
exit(-1);
}
int* p = a + 1;
free(p);
p = NULL;
return 0;
}
运行截图:
为了防止出现这种问题,我们尽量做到,空间是谁申请的就由谁去释放。