上期我们手搓出了一个通讯录,当时是创建了一个名为context的结构体,里面又有装着人的信息的数组peo_info,我们创建了100个这个类型的数组,因此内存是非常大的,如果我们只在里面存了5~6个人的信息那岂不是大材小用??所以此时就有一个思路,我们把初始时候的大小设为5个,满了就+2个,以此类推,那么空间就不会被浪费太多对吧,所以C语言中动态内存的知识就来咯~
一、动态内存函数介绍
1.1 malloc和free
C语言提供了动态内存开辟的函数malloc
void* malloc(size_t size)//动态内存的开辟
这个函数会向内存申请一个 连续可用的空间,并返回空间的起始地址(指针)
另外还需要注意以下4点:
1. 如果开辟空间成功,则返回空间的指针
2. 如果开辟空间失败,则返回空指针,因此开辟完空间的时候需要我们去检查一下这个指针是否为空
3. 返回类型为void*,因为他并不知道我们要开辟的这块空间的类型是什么样的,所以我们在接收指针的时候需要强制类型转换一下
4. 当size = 0的时候,纯纯犯贱,此时连系统都看不下去懒得救你
来举个例子,假设我们需要创建大小为num的int型数组,我们可以这样写:
int num = 0;
scanf("%d", &num);
int arr[num] = { 0 };
但是这样写在VS的环境下是不行滴,所以我们就需要改为动态内存
int num = 0;
scanf("%d", &num);
int* ptr = (int*)malloc(sizeof(int) * num);//动态内存开辟
if (ptr == NULL)//检测是否开辟成功
{
perror("malloc:");
return 1;
}
之后就可以进行对应的操作啦,当你操作完了之后,要记得向内存还回去这个空间,不要有借无还噢,因此我们就需要用到空间释放的函数free
C语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的
void free (void* ptr);//空间的释放
需要注意的是如果ptr为空指针那么函数什么都不干,空间释放完了之后他不会把ptr置为空指针,因此为了防止ptr成为野指针,需要我们自己将ptr置为空指针
int num = 0;
scanf("%d", &num);
int* ptr = (int*)malloc(sizeof(int) * num);//动态内存开辟
if (ptr == NULL)//检测是否开辟成功
{
perror("malloc:");
return 1;
}
//操作
//...
//释放
free(ptr);
ptr = NULL;
这样才正确使用了动态内存的开辟和释放
1.2 calloc
calloc和malloc类似,都是动态内存开辟的函数
void* calloc (size_t num, size_t size);
不一样的是calloc需要我们输入两个参数,第一个是元素的个数,第二个是元素的大小,开辟之后的空间它会全部帮我们初始化为0
int main()
{
int num = 0;
scanf("%d", &num);
int* ptr = (int*)calloc(num, sizeof(int));//动态内存开辟
if (ptr == NULL)//检测是否开辟成功
{
perror("calloc:");
return 1;
}
//操作
//...
//释放
free(ptr);
ptr = NULL;
return 0;
}
malloc是不会帮你初始化这块空间的哟,所以我个人认为calloc更方便一些
1.3 realloc
动态内存的动来了,realloc函数的出现让动态内存管理更加灵活,它可以使我们申请的空间大小变为我们想要的大小
void* realloc (void* ptr, size_t size);
显然第一个参数是开辟空间的指针,第二个是改变后的空间大小(字节)
需要注意的是当他改变空间成功之后还是返回这个空间的指针,但是如果没有开辟成功的话返回的是一个空指针(NULL),因此我们应该先检测一下返回的指针是否为空指针,然后再造作,以防万一找不到之前开辟的空间
realloc调整空间有两种方式;
1. 如果后面有足够多的连续空间,那么可以直接将需要增加的空间放后面(连续)
2. 如果后面连续的空间不够,函数会在堆区找到合适大小的空间,返回新地点的指针
int main()
{
int* ptr = malloc(100);
if (ptr == NULL)
{
perror("malloc:");
return 1;
}
//操作
//……
//增加空间
int* p = realloc(ptr, 200);
if (p)//先检测p是否为空指针,以防空间丢失
{
ptr = p;
}
else
{
perror("realloc:");
return 1;
}
free(ptr);//释放空间
return 0;
}
二、通讯录2.0版(动态内存版)
有了上述的知识之后,我们就可以将我们的通讯录弄得更为完善
2.1 结构体context的修改
这是我们上一个静态版本的context结构体
//静态版本
typedef struct context
{
peo_info data[MAX];//存放人的信息
int sz;//存放当前存的人的个数
}context;
我们用的是一个100大小的数组来控制我们的通讯录的大小,我们要将它改为动态内存版的就不需要数组了,只需要一个指针(指向开辟好的内存空间)就行,然后我们还需要一个值来纪律当前的内存大小是多少,方便之后增加内存
//动态版本
typedef struct context
{
peo_info* data;//存放人的信息
int sz;//存放当前存的人的个数
int capacity;//当前通讯录的最大容量
}context;
2.2 初始化函数的修改
这是我们上个版本的初始化函数
//初始化通讯录
void init_context(context* pc)
{
assert(pc);
pc->sz = 0;
memset(pc->data, 0, sizeof(pc->data));
}
现在我们要先申请5个context字节大小的空间,并初始化,所以直接用calloc函数,申请并初始化空间,再把当前申请好的空间初始化为5
void init_context(context* pc)
{
assert(pc);
pc->sz = 0;
peo_info* ptr = (peo_info*)calloc(DEFAULE_SZ,sizeof(peo_info));
if (ptr == NULL)
{
perror("init_context::calloc");
return;
}
pc->data = ptr;
pc->capacity = DEFAULE_SZ;
}
2.3 增加空间
每次增加联系人的时候都检查一下空间满了没有,如果满了,就像内存再申请2个空间
//动态版本
void check_capacity(context* pc)
{
assert(pc);
if (pc->sz == pc->capacity)
{
//增加容量
peo_info* ptr = realloc(pc->data, (pc->capacity + INC_SZ) * sizeof(peo_info));
if (NULL == ptr)
{
perror("check_capacity::realloc");
return;
}
pc->data = ptr;
pc->capacity += INC_SZ;
printf("增加容量成功\n");
}
}
void add_context(context* pc)
{
assert(pc);
check_capacity(pc);
//增加一个人的信息
printf("请输入名字:");
scanf("%s", pc->data[pc->sz].name);
printf("请输入年龄:");
scanf("%d", &pc->data[pc->sz].age);
printf("请输入性别:");
scanf("%s", pc->data[pc->sz].sex);
printf("请输入电话:");
scanf("%s", pc->data[pc->sz].tele);
printf("请输入地址:");
scanf("%s", pc->data[pc->sz].addr);
pc->sz++;
printf("添加成功\n");
}
这样我们就能得到一个内存空间灵活的通讯录
三、动态内存常见的错误
3.1 对NULL解引用
int main()
{
int* ptr = malloc(100);
*ptr = 20;//如果没有开辟成功,ptr就为空指针
free(ptr);
return 0;
}
因此我们需要先检查malloc是否返回的是空指针
int main()
{
int* ptr = malloc(100);
if (ptr == NULL)
{
perror("malloc:");
return 1;
}
*ptr = 20;//如果没有开辟成功,ptr就为空指针
free(ptr);
return 0;
}
3.2 对动态开辟空间的越界访问
int main()
{
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);
return 0;
}
3.3 对非动态开辟内存使用free释放
int main()
{
int a = 10;
int* p = &a;
free(p);//这样可以吗?
return 0;
}
3.4 使用free释放一块动态开辟内存的一部分
int main()
{
int* p = (int*)malloc(100);
p++;
free(p);//p不再指向动态内存的起始位置
}
3.5 重复释放
int main()
{
int* p = (int*)malloc(100);
free(p);
free(p);//多次释放
}
3.6 动态开辟内存忘记释放(内存泄漏)
void test()
{
int *p = (int *)malloc(100);
if(NULL != p)
{
*p = 20;
}
}
int main()
{
test();
while(1);
}