什么是动态内存?
- 所谓动态内存分配,就是指在程序执行的过程中动态地分配或者回收存储空间的分配内存的方法。动态内存分配不像数组等静态内存分配方法那样需要预先分配存储空间,而是由系统根据程序的需要即时分配,且分配的大小就是程序要求的大小。
为什么存在动态内存分配
我们已经掌握的内存开辟方式有:
int val = 20;//在栈空间上开辟四个字节
char arr[10] = {
0};//在栈空间上开辟10个字节的连续空间
但是上述的开辟空间的方式有两个特点:
- 空间开辟大小是固定的。
- 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。
但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足了。 这时候就只能试试动态存开辟了。
动态内存函数介绍
malloc
函数原型,头文件stdlib.h
void *malloc( size_t size );
这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
- 如果开辟成功,则返回一个指向开辟好空间的指针。
- 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
- 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
- 如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。
代码示例
#include<stdio.h>
#include<stdlib.h>
int main()
{
int arr[10] = {
0 };//在栈区上开辟40个字节
int* p = (int*)malloc(40);//动态内存开辟的空间在堆区上
if (p == NULL) //判断是否为空指针,如果是空指针,则空间开辟失败
{
//开辟空间失败。
printf("动态内存开辟失败");
return 1;//开辟失败提前返回
}
else
{
//开辟空间成功,使用空间
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = 0;
}
}
return 0;
}
根据上面代码我们调试看到,开辟成功后p是指向这块动态内存的,并且内存放的是随机值
我们可以将它看作一个数组,解引用p+i来使用这段内存。
通过调试可以发现,p指向的空间都被赋值为0了。
free
- C语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的。
函数原型,头文件stdlib.h
void free (void* ptr);
- 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
- 如果参数 ptr 是NULL指针,则函数什么事都不做。
代码示例
#include <stdio.h>
#include<stdlib.h>
int main()
{
int* ptr = NULL;
int num = 0;
scanf("%d", & num);
ptr = (int*)malloc(num * sizeof(int));
if (NULL != ptr)//判断ptr指针是否为空
{
int i = 0;
for (i = 0; i < num; i++)
{
*(ptr + i) = 0;
}
}
//当不在使用ptr指向的动态内存时需要释放动态内存
free(ptr);//释放ptr所指向的动态内存
ptr = NULL;//是否有必要?
return 0;
}
关于最后ptr置NULL,ptr指向的空间被释放后,ptr成了一个野指针,这时如果不小心使用了ptr指向的空间,后果是不可知,所以说ptr的指向的空间被释放后,就没有打算在使用ptr这个指针了,不管是为了方便也好,安全也罢,这里都推荐动态内存释放后,指针置NULL。
切记: 动态开辟的空间一定要释放,并且正确释放 。忘记释放不再使用的动态开辟的空间会造成内存泄漏。
calloc
- C语言还提供了一个函数叫 calloc , calloc 函数也用来动态内存分配。
函数原型
void* calloc (size_t num, size_t size);
- 函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。
- 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。
使用方法
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p = (int*)calloc(10, sizeof(int));//开辟十个整型大小
if (p != NULL)//判断是否开辟失败
{
//使用空间
}
free(p);
p = NULL;
return 0;
}
其实calloc函数跟malloc函数没多大区别,唯一的区别就是,calloc内存开辟成功后会把内存初始化为0。
所以以后碰到动态开辟内存,如果内存需要初始化就用calloc,不需要初始化就用malloc。
realloc
- realloc函数的出现让动态内存管理更加灵活。
- 有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时候内存,我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小的调整。
函数原型
void* realloc (void* ptr, size_t size);
- ptr 是要调整的内存地址
- size 调整之后新大小
- 返回值为调整之后的内存起始位置。
- 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
代码示例
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
//开辟空间失败。
printf("动态内存开辟失败");
return 1;//开辟失败提前返回
}
else
{
//开辟空间成功,使用空间
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
}
//追加空间
int* pa = (int*)realloc(p, 80);
if (pa == NULL)//调整空间失败
{
return 1;
}
else
{
p = pa;
//使用调整空间
for (int i = 10; i < 20; i++)
{
*(p + i) = i;
}
}
//释放空间
free(p);
p = NULL;
return 0;
}
realloc在调整内存空间的是存在两种情况
- 情况1:原有空间之后有足够大的空间
当是情况1 的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。
- 情况2:原有空间之后没有足够大的空间
当是情况2 的时候,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址。 由于上述的两种情况,realloc函数的使用就要注意一些。
示例
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
//开辟空间失败。
printf("动态内存开辟失败");
return 1;//开辟失败提前返回
}
else
{
}
int* pa = (int*)realloc(p, 80);//使用辅助指针接受调整的空间
if (pa == NULL)//判断调整空间是否失败
{
return 1;
}
else
{
p = pa;//调整空间成功后在把指向的空间赋给原指针
}
//释放空间
free(p);
p = NULL;
return 0;
}
使用realloc的时,需注意,调整空间最好使用一个辅助指针来接受,调整成功在把辅助指针指向的空间赋给原指针,因为如果用原指针直接接受调整空间的话,万一调整失败,原指针被置为NULL,这样就找不到原先开辟好的那段内存了。
动态版通讯录
上次写了一篇静态版通讯录的博客,在学习动态内存开辟后就可以来改造一下通讯录了,我们来看看动态版通讯录有哪些改动。
没看过静态版通讯录的读者可以先去我这篇博客了解一下静态版通讯录
改动一: 通讯录结构体
静态版
struct Contact
{
struct PeoInfo data[DATA_MAX];
int sz;//记录通讯录中里有几个人的信息
};
静态版使用了固定大小的内存
动态版改动
//通讯录的结构体
struct Contact
{
struct PeoInfo* data;
int sz;//记录通讯录中里有几个人的信息
int space;//当前最大内存
};
把数组改成指针方便接受开辟的动态内存,以及增加了一个space的变量,记录当前的最大内存是多少。
改动二:初始化通讯录
静态版
//初始化通讯录
void InitContact(struct Contact* con)
{
con->sz = 0;
//memset -设置内存的函数
memset(con->data, 0, sizeof(con->data));
}
静态版把数组的所有元素置0。
动态版改动
#define MEMORY_SZ 3 //初始内存
void InitContact(struct Contact* con)
{
con->sz = 0;
con->space = MEMORY_SZ;
struct PeoInfo* tmp = (struct PeoInfo*)malloc(MEMORY_SZ * sizeof(struct PeoInfo));
if (tmp == NULL)//判断是否开辟失败
{
printf("%s\n", strerror(errno));
exit(1);
}
else
{
con->data = tmp;
}
}
在初始通讯录的时候设定最大内存为3(自己想设多少都行),使用通讯录指针开辟3个存放联系人的空间。
改动三:增加联系人
静态版
//增加联系人
void Addcontact(struct Contact* con)
{
if (con->sz == DATA_MAX)
{
printf("通讯录已满,无法继续添加\n");
}
else
{
printf("添加联系人\n");
printf("请输入姓名:");
scanf("%s", con->data[con->sz].name);
printf("请输入年龄:");
scanf("%d", &con->data[con->sz].age);
printf("请输入性别:");
scanf("%s", con->data[con->sz].sex);
printf("请输入电话:");
scanf("%s", con->data[con->sz].tele);
printf("请输入地址:");
scanf("%s", con->data[con->sz].addr);
con->sz++;
printf("添加成功\n");
}
}
静态版添加联系人,假设满人了则无法继续添加
动态版改动
//增加联系人
void Addcontact(struct Contact* con)
{
if (con->sz == con->space)
{
struct PeoInfo* tmp = realloc(con->data,(con->space + 2) * sizeof(struct PeoInfo));//使用辅助指针接受调整的空间
if (tmp == NULL)//判断增容是否失败
{
printf("%s\n", strerror(errno));
return;
}
else
{
con->data = tmp;
con->space += 2;
printf("增容成功\n");//增容成功打印提示一下
}
}
printf("添加联系人\n");
printf("请输入姓名:");
scanf("%s", con->data[con->sz].name);
printf("请输入年龄:");
scanf("%d", &con->data[con->sz].age);
printf("请输入性别:");
scanf("%s", con->data[con->sz].sex);
printf("请输入电话:");
scanf("%s", con->data[con->sz].tele);
printf("请输入地址:");
scanf("%s", con->data[con->sz].addr);
con->sz++;
printf("添加成功\n");
}
动态版如果达到了最大内存,就使用realloc增加空间,增加多少空间看需求,我这里是增加的2个联系人的空间方便调试,并且把最大内存加2。
运行截图
emmmm,动态版通讯录就改动了这些东西,以及添加了退出程序的时候释放动态内存。
void FreeMemory(struct Contact* con)//释放开辟的内存
{
free(con->data);
con->data = NULL;
con->space = 0;
con->sz = 0;
}
释放动态开辟的内存,并把指针置NULL。
小结
-
动态版通讯录可以按需调整空间大小,相比静态版通讯录而言,更能合理的运用内存。
-
动态版缺点就是无法保存联系人信息到一个文件当中去,每次执行程序都需要重新录入联系人。
-
文件操作可以弥补动态通讯录的缺点,保存在文件当中,下篇博客我将会讲文件版通讯录改造。