深入理解动态内存管理(C语言)
深入理解动态内存管理(C语言)
为什么有动态内存管理
int a = 0;
int val[10];
在学习动态内存管理之前,我们开辟的空间大小往往是固定的
即使开辟一个数组,也需要在创建的时候确定长度,并且大小无法修改
然而我们在开辟空间时,可能会遇到上述之外的情况,比如在程序运行时,才知道需要的空间大小
这时候有人说,可以开辟一个很大的空间
但是这个很大的空间也是有大小上限的,而且有时候还会造成空间的过度浪费
于是C语言引入了动态内存开辟,让程序员可以灵活地申请和释放空间
C语言在进行动态内存管理的,会涉及四个函数
malloc
和free
malloc
首先我们来看第一个函数malloc
,函数原型如下:
void* malloc(size_t size)
功能:向内存中的堆区申请一块size
大小连续可用的空间,并且返回这块空间的起始地址
- 如果开辟成功,则返回这块空间的起始地址
- 如果开辟失败,则返回
NULL
,因此开辟完成后需要检查 - 返回的指针类型是
void*
,是为了让使用者自己决定空间类型 - 参数
size
单位是字节,如果为0,malloc
的行为是标准未定义的,取决于编译器
现在,我们可以尝试使用malloc
#include <stdio.h>
#include <stdlib.h>//动态内存管理函数相关的头文件int main()
{//假设向内申请20个byte,存放5个整数int* p = (int*)malloc(5 * sizeof(int));//判断返回值if (p == NULL)//开辟失败,打印错误信息{perror("malloc");//malloc :.....return 1;}//开辟成功,使用空间for (int i = 0; i < 5; i++){*(p + i) = i + 1;}for (int i = 0; i < 5; i++){printf("%d ", p[i]);}return 0;}
ok,这里成功开辟了
当然,有时候也会有开辟失败的时候,例如开辟的空间太大
这里我们把这段代码
int* p = (int*)malloc(5 * sizeof(int));
改成这样
int* p = (int*)malloc(INT_MAX * sizeof(int));
在x86环境下,就可以看到,开辟失败了,并且打印的错误信息是:没有足够空间
所以,我们在使用malloc
去申请空间的时候,一定要去检查返回值
free
有借有还,再借不难
我们在向内存申请完空间之后,还需要主动去释放归还所申请的空间
否则这块空间就会在程序运行期间,一直占用
我们的free
函数就是用来释放动态申请的空间的
void free(void* ptr)
功能:释放和回收动态开辟的内存
- 如果参数
ptr
指向的空间不是动态开辟的,那free函数行为是未定义的 - 如果
ptr
为NULL
,则函数什么都不做
所以对于上面使用malloc的代码,我们需要对它申请的空间进行释放
#include <stdio.h>
#include <stdlib.h>//malloc和free
int main()
{//假设向内申请20个byte,存放5个整数int* p = (int*)malloc(5* sizeof(int));//判断返回值if (p == NULL)//开辟打印,错误信息{perror("malloc");//malloc :.....return 1;}//开辟成功,使用空间...//不再使用这块空间,我们要把它回收释放、free(p);//释放这块空间return 0;
}
但是这样就好了吗?
我们来调试一下,看看释放空间后指针p的情况
上面是我们释放完空间之后,指针p指向的内存空间情况
可以看到,虽然已经对这块空间进行释放了,但是这块空间并没有置为NULL
,这就导致了p成为野指针
所以,我们在使用完free
释放掉内存之后,还需要手动把指针置空
#include <stdio.h>
#include <stdlib.h>//malloc和free
int main()
{//假设向内申请20个byte,存放5个整数int* p = (int*)malloc(5* sizeof(int));//判断返回值if (p == NULL)//开辟打印,错误信息{perror("malloc");//malloc :.....return 1;}//开辟成功,使用空间...//不再使用这块空间,我们要把它回收释放、free(p);//释放这块空间,但是不会将p置空p = NULL;//置空return 0;
}
上面就是我们动态内存管理中,很常见的一对函数
calloc
和realloc
除了上面两个函数之外,C语言中还有两个用来动态内存分配的函数``
calloc
我们首先来看calloc
函数
void* calloc(size_t num,size_t size)
功能:在堆区开辟num
个大小为size
(单位字节)的空间,并且把空间的每个字节初始化为0
- 与
malloc
的区别只在于参数拆开了和开辟空间的初始化了
我们可以直接打印开辟好的空间来看看
#include <stdio.h>
#include <stdlib.h>
//malloc
int main()
{//申请10个整型的空间int* p = (int*)malloc(10 * sizeof(int));//判断返回值if (p == NULL){perror("calloc");return 1;}for (int i = 0; i < 10; i++){printf("%d\n", p[i]);}free(p);//释放空间p = NULL;//置空return 0;
}
#include <stdio.h>
#include <stdlib.h>
//calloc
int main()
{//申请10个整型的空间int* p = (int*)calloc(10, sizeof(int));//判断返回值if (p == NULL){perror("calloc");return 1;}for (int i = 0; i < 10; i++){printf("%d\n", p[i]);}free(p);//释放空间p = NULL;//置空return 0;
}
可以看到malloc
开辟的空间没有初始化
而calloc
初始化了为0
realloc
有时候,我们动态开辟的空间可能会大了或者小了,这时候我们就可以使用realloc
函数对动态内存进行大小的调整,让动态内存管理变得更加灵活
函数原型:
void* realloc(void* ptr,size_t size)
功能:在堆区把ptr
指向的动态开辟的空间大小,调整为size
(单位字节),并且返回调整后空间的地址
我们来模拟使用一下:
#include <stdio.h>
#include <stdlib.h>
//realloc
int main()
{//申请一块空间存放1-5int *p = (int*)calloc(5, sizeof(int));//判断返回值if (p == NULL){perror("calloc");return 1;}for (int i = 0; i < 5; i++){p[i] = i + 1;}//想扩大,继续放6-10p = (int*)realloc(p, 40);for (int i = 5; i < 10; i++){p[i] = i + 1;}
//释放置空free(p);p = NULL;return 0;
}
我们来看这段代码
它用原来的指针p
,来接收了新开辟的空间,但是注意,我们realloc
在开辟空间的时候,也有可能会开辟失败,一旦失败,p为NULL
,对它解引用会报错,并且原来的数据也会丢失
所以,我们一般会再创建一个指针变量来接收新地址,确认成功开辟后再把这个地址给p
//想扩大,继续放6-10
int *p2 = (int*)realloc(p, 40);
//判断返回值
if (p2 == NULL)
{//业务处理
}
//增加成功
p = p2;
我们对这段代码进行调试
可以看到,其实realloc
动态开辟后的空间和原来p的空间起始地址不一样,不是原本的那块空间
其实这是因为realloc
在调整空间的时候会遇到两种情况:
①原有空间之后有足够的空间
②原有空间之后没有足够空间
对于情况①,要扩展空间,只需要在原来内存之后增加空间,原数据不会动
但对于情况②,会先在堆区找一块大小合适的空间,把原有数据拷贝过来,再释放原空间,之后返回新空间地址
由于存在上述两种情况,所以我们在使用realloc
的时候,需要注意一下
其实我们的realloc
函数还可以直接用来动态开辟空间
int *p = (int*)realloc(NULL,40)
当我们第一个参数传NULL
时,realloc
就会直接开辟size
字节大小的空间,效果和malloc
一样
- 上述函数,均包含在
stdlib.h
头文件中
动态内存管理中常见的错误
通过上面的介绍,可以发现,我们在进行动态内存管理的时候,其实是有很多需要注意的地方的,于是我总结了六个比较常见错误,希望可以帮助到大家
①对NULL
指针的解引用
void test()
{
int *p = (int *)malloc(INT_MAX/4);
*p = 20;//如果p的值是NULL,就会有问题
free(p);
}
对于动态申请的空间,需要先判断返回值再使用
②对动态开辟空间的越界访问
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);
}
③对非动态开辟空间使用free
来释放
void test()
{
int a = 10;
int *p = &a;
free(p)
}
④使用free
去释放动态开辟空间的一部分空间
void test()
{
int *p = (int *)malloc(100);
p++;
free(p);//p不再指向动态内存的起始位置
}
⑤对同一块动态内存多次释放
void test()
{
int *p = (int *)malloc(100);
free(p);
free(p);//重复释放
}
⑥动态开辟内存忘记释放(内存泄漏)
void test()
{
int *p = (int *)malloc(100);
if(NULL != p)
{
*p = 20;
}
}
int main()
{
test();
while(1);
return 0;
}
一些建议
- 谁申请的空间,谁释放
- 不使用的空间,及时释放
- 自己(函数1)不方便释放,要告诉别人(函数2)释放
malloc
和free
一般成对出现
总结
以上就是我们动态内存管理相关的内容了,如果有什么问题,可以评论留言,感谢支持