一.为什么要有动态内存分配
我们知道,当我们创建变量的时候,我们会向系统申请一定大小的空间内存。比如int a=10或者int arr[10];我就向内存申请了4或者40个字节的大小来存放数据。但是当我们一旦申请好这个空间,大小就无法调整了。但是对于空间的需求,不仅仅就只有上面的情况。有时候我们需要的空间大小只有在程序运行的时候才能知道,那么数组编译时开辟空间的方式就不能满足了。
在C语言中,引入了动态内存开辟,让程序员可以自己申请和释放空间,就比较灵活了。
注意,以下介绍的函数头文件都是stdlib.h
二.malloc,free,calloc和realloc
1.malloc函数的理解
这个函数可以向内存申请一块连续可用的空间,并且返回指向这块空间的指针。
1.如果开辟空间成功,则返回一个指向开辟好的空间的指针。
2.如果开辟失败,则返回一个NULL指针,因此我们在使用malloc的时候一定要检查。
3.因为它的返回值是void*,malloc函数不知道返回值的类型,具体在使用时还是程序员来定。
4.如果参数size(单位是字节)为0,malloc的行为是标准没有定义的,取决于编译器。
比如我想开辟能存放5个整数的空间
int main()
{
int* p = (int*)malloc(20);//这里开辟20个字节的空间,把起始地址放在p里
if (p == NULL)//判断也没有开辟成功
{
perror("malloc");
return 1;
}
int i = 0;
for (i = 0; i < 5; i++)
{
*(p +i) = i + 1;
}
return 0;
}
我们可以调试看一下在内存中它是怎么开辟的。
代码运行完毕后:
这些就是malloc函数的使用了。还有我上面写的第4条,明明我用malloc是来申请空间的,结果我要申请0个字节的空间,这就显得有点没用了。
我们需要注意的一点,这些开辟的空间都在内存的哪一个区域?不管是malloc,free,calloc或者realloc这些跟动态内存有关的都存放在内存的堆区。而我们创建的像是局部变量,形式参数这些是在栈区。而静态变量,全局变量,这些在静态区。
2.free函数的理解
这个函数是专门用来做动态内存的释放和回收的。
1.如果参数ptr指向的空间不是动态内存开辟的,那free函数的行为的未定义的。
2.如果参数ptr是NULL指针,则函数什么事也不做。
也就是说,我想释放哪里的空间,就把这一块空间的起始地址给这个函数的参数。
free的作用就是把空间的使用权限还给了操作系统,还是上面的代码:
#include<stdlib.h>
int main()
{
int* p = (int*)malloc(20);//这里开辟20个字节的空间,把起始地址放在p里
if (p == NULL)//判断也没有开辟成功
{
perror("malloc");
return 1;
}
int i = 0;
for (i = 0; i < 5; i++)
{
*(p +i) = i + 1;
}
free(p);
p = NULL;
return 0;
}
调试运行之后是这样的:
注意,我们在把p指向的那一块空间给释放了之后,p指针还是存在的,但是它没有指向的东西了,此时的p就是一个野指针,那我们就必须给这个野指针一条绳子NULL来栓住它。
3.calloc函数的理解
calloc函数也是用来动态申请空间的,但是它的用法跟malloc不太一样。
1.函数的功能就是为num个大小位size的元素开辟一块空间,并且把空间的每个字节初始化为0。
2.与malloc的区别就是,malloc在开辟空间的时候没有初始化这个功能。
举个例子:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p = (int*)calloc(10, sizeof(int));
if (p = NULL)
{
perror("calloc");
return 1;
}
return 0;
free(p);
p = NULL;
}
这里我没有进行赋值操作,但是在内存中这些值就已经变成了0。
其他的地方跟malloc是一样的,在释放空间后也是需要给上一个NULL。
4.realloc函数的理解
这个函数是基于动态内存的,如果我们在开辟空间的时候,觉得我们申请的空间太大了或者太小了,为了能够合理的使用内存,我们可以使用realloc函数实现对动态开辟内存大小的调整。
1.ptr是要调整的内存地址,size是调整之后的大小。
2.返回值是调整之后的内存起始位置。
3.这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
4.realloc函数在调整时有两种情况,一种是原来的空间后面有足够的空间大小,另一种是后面的空间不够了。
我先简单写一下代码,简单说一下第四点会出现的问题。
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p = (int*)calloc(5, sizeof(int));//这里原本是20个字节的大小
if (p = NULL)
{
perror("calloc");
return 1;
}
int* ptr=(int*)realloc(p, 40);//这里调整为40个字节
if (ptr != NULL)//这里我没有直接把开辟空间的起始地址给p,就是害怕万一开辟空间失败了,原来的空间也没了
{
p = ptr;//这里空间调整成功了就可以使用我们开辟的40个字节的空间了
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i + 1;
}
free(p);//一定要记得释放,否则会存在内存泄漏的问题
p = NULL;//释放空间了一定要置为空指针
}
else
{
perror("realloc");
}
return 0;
}
这里就是开辟空间扩大的情况。
情况一:原内存之后有足够的空间的话,会直接在原来的空间的后面再开辟20个字节,凑够40个字节。
情况二:原内存之后没有足够的空间的话,这个函数会在堆区的内存里找一个新的空间,并且是满足我们所要开辟的空间的,而且会把原来空间的数据拷贝一份到新的空间,原来的空间释放掉,并且会把新的空间的起始地址返回。
情况三:如果调整失败了,直接返回NULL。
注意:realloc不仅仅可以调整空间的,它也可以开辟空间,我们想一下,如果我给ptr的参数是一个空指针呢?
#include<stdlib.h>
int main()
{
realloc(NULL, 20);//这个等价于malloc(20)
return 0;
}
以上就是动态空间分配需要使用到的函数。
5.总结4个函数
当我们在申请空间后,不再用这块空间了,虽然,当程序结束的时候,操作系统会回收这一块空间,但最好是我们主动去用free释放,尽量做到谁(函数)申请的空间谁释放。因为如果我的这块空间一直不退出的话这一块内存就一直被占用着,谁也用不了。如果不能释放,要告诉使用的人,要记得释放。
三.柔性数组
关于柔性数组,有几点需要介绍一下:
1.它存在于结构体中,是结构体的最后一个成员
2.最后的一个成员是数组,而且没有指定大小
struct S
{
char c;
int i;
int arr[];//这个就是柔性数组,并没有规定具体的大小或者写成int arr[0]
};
这就是柔性数组
1.柔性数组的特点
1.结构体里的柔性数组成员前面必须至少有一个其他成员
2.sizeof计算这样含柔性数组大小的时候,不包括柔性数组的内存
3.包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
关于前两点
直接就只是i的大小。
再来看第三点:
struct S
{
int i;
int arr[];//这个就是柔性数组,并没有规定具体的大小
};
#include <stdio.h>
int main()
{
struct S* ps=(struct S*)malloc(sizeof(struct S) + 5 * sizeof(int));
free(ps);
p=NULL;
return 0;
}
除了我要开辟的int大小的空间,后面我又加了一个5 * sizeof(int)这个是我要额外开辟的空间。
我们既然是用malloc开辟的空间,那么我们是不是就可以使用realloc来改变这一块空间,随便怎么变大变小。对应到这个数组上,我们不就是把这个数组变成“柔性”了吗?
2.柔性数组的使用
了解了它的一些基本特点,还有前面的几个函数,使用起来也就很简单了。
struct S
{
int i;
int arr[];//这个就是柔性数组,并没有规定具体的大小
};
#include <stdio.h>
int main()
{
struct S* ps=(struct S*)malloc(sizeof(struct S) + 5 * sizeof(int));
if (ps == NULL)//没有开辟成功
{
return 1;
}
ps->i = 100;
int a = 0;
for (a = 0; a < 5; a++)
{
ps->arr[a] = a + 1;
}
struct S* ret = (struct S*)realloc(sizeof(struct S) + 10 * sizeof(int));//调整为44个字节大小
if (ret != NULL)
{
ps = ret;
}
free(ps);
ps = NULL;
return 0;
}
到这里,关于动态内存管理的相关知识就已经讲完了。感谢大家的观看,如有错误,请大家多多指正。