【C语言进阶】动态内存管理(2)
之前我们在上一期内容介绍了两个函数一个是malloc一个是free,也讲解了这两个函数的细节以及原理,这一期内容我们将剩下的几个函数进行全部讲解,争取让每一个读者能够看懂。
目录
1. calloc函数
2. realloc函数
2.1 realloc的工作原理
3.动态扩容版通讯录
3.1结构体修改:
3.2 初始化函数修改
3.3 增加函数修改
3.4 回收空间
4. 常见的动态内存的错误
4.1 空指针解引用
4.2 对动态开辟的空间的越界访问
4.3 对非动态开辟的空间进行free释放
4.4 使用free释放动态内存开辟的一部分
4.5 对同一块内存空间的多次释放
4.6 动态内存忘记释放(内存泄漏)
5. 经典面试题
5.1 看代码说输出结果
5.2 指出下面程序的问题
1. calloc函数
通过函数参数描述我们可以发现,第一个参数是num代表元素的个数,第二个参数size代表每个元素的大小(字节);这个函数能够在返回首地址的时候进行初始化。例如下面的代码:
#define _CRT_SECURE_NO_WARNINGS#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include<errno.h>int main()
{// 开辟十个整型的空间int* p = (int*)calloc(10, sizeof(int));if (p == NULL){printf("%s\n", strerror(errno)); // 若指针为空打印错误信息return 1;}int i = 0;for (i = 0; i < 10; i++){printf("%d ",p[i]);}return 0;
}
我们发现使用calloc进行动态内存分配的空间是能够自动初始化的,并且初始化为0;
calloc = malloc + memset,功能更加的强大;到这里为止还是没有将如何“动态”地进行内存分配,下面的函数就会介绍到这一点。
2. realloc函数
void* realloc(void* ptr,size_t size);
有时候我们分配完空间就会发现,空间过小或者过大,此时需要进行调整。
参数1:ptr是进行调整的空间的起始位置(堆空间);
参数2:size是新空间的字节数。
2.1 realloc的工作原理
假如我们现在有40个字节的空间,现在需要扩容到80个字节,有两种情况:
①第一种情况:在原来的内存后面继续开辟空间,但是后面已经被其他数据占用了,那么我们需要单独找一块完整的80字节的空间,将所有数据挪过来,再把80字节空间的首地址返回,此时旧的空间会被自动释放;
②第二种情况: 在原来的内存空间后面有足够的内存空间,所以只需要在后面进行扩容即可。
那么能不能申请完空间直接再赋给p呢,这里是不行的,以为rrealloc有可能申请空间失败,此时就会返回一个空指针,那么p本来指向40个字节的空间的,但是由于扩容失败,导致p将之前40个字节的数据全部丢失。
正确扩容姿势如下:
int *p = (int*)malloc(40);
int *ptr = realloc(p,80);
if(ptr != NULL)
{p = ptr;
}
动态开辟空间这么好用,那为什么不直接动态开辟内存呢?这是因为多次开辟空间之后,空间与空间之间会产生大量内存碎片,如果这些内存碎片没有及时利用就会造成空间的浪费。
realloc的第一个参数如果填空指针,那么他的功能就和malloc一样了。
3.动态扩容版通讯录
这个通讯录的项目,博主在之前已经讲过了,如果需要可以移步:通讯录项目;
这里需要给这个项目进行升级,之前的通讯录是在栈上开辟空间,这样会造成资源的浪费,所以这个版本需要使用动态内存扩容。
需求:
1.通讯录默认存放3个人的信息;
2.空间不够,每次增加2个人的空间。
3.1结构体修改:
需要将通讯录中的结构体数组换成结构体指针;由于是动态扩容,所以需要给一个变量标注最新的容量。
// 动态版本
typedef struct Contact
{// 通讯录假如可以存放一百条信息PeoInfo* data;// 真实存放的信息的条数int count;// 动态空间的容量int capacity;
}Contact;
本质上就是把数组换成了一个指针(堆区);
3.2 初始化函数修改
修改初始化函数,将通讯录的data给一个分配好空间的起始地址,然后将容量设置为初始容量3;
// 动态初始化
void InitContact(Contact* p)
{assert(p);p->count = 0;// 分配空间并且赋初值p->list = (PeoInfo*)calloc(3, sizeof(PeoInfo));if (p->list == NULL){printf("%s\n", strerror(errno));}p->capacity = 3;
}
3.3 增加函数修改
剩下的只需要更改add方法即可。首先需要判断如果通讯录满了就需要扩容,其实就是使用relloc扩容完毕返回一个地址,判断地址是否为空指针,若不是空指针就把地址给通讯录中的指针data,然后容量+1即可,剩下的内容保持不变。
// 通讯录的增加方法
void addContact(Contact* p)
{assert(p);if (p->count == p->capacity){// 扩容PeoInfo* tmp = realloc(p->data,(p->capacity + 2) * sizeof(PeoInfo));if (tmp == NULL){printf("%s\n", strerror(errno));return;}p->data= tmp;p->capacity += 2;printf("扩容成功!\n");}printf("请输入姓名:\n");scanf("%s", (p->list)[p->count].name);printf("请输入年龄:\n");scanf("%d", &((p->list)[p->count].age));printf("请输入性别:\n");scanf("%s", (p->list)[p->count].gender);printf("请输入电话号码:\n");scanf("%s", (p->list)[p->count].tele);printf("请输入地址:\n");scanf("%s", (p->list)[p->count].addr);(p->count)++;printf("通讯录增加成功!\n");
}
当通讯录的联系人超过3人,此时再添加联系人会进行扩容,如下所示:
3.4 回收空间
之前我们在堆处开辟了空间,当我们选择退出程序的时候需要把堆空间的数据清空,指针指向空,所以需要再补充一个函数。
我们知道通讯录的data数组是在堆空间申请的,所以只需要销毁data数组就行。
// 通讯录数据销毁
void destroyMem(Contact* p)
{if (p == NULL) {printf("%s\n",strerror(errno));}free(p->data);p->data= NULL;
}
4. 常见的动态内存的错误
4.1 空指针解引用
int* p = malloc(40);
*p = 20;
正确做法:需要判断指针是否为空
int* p = malloc(40);if (p == NULL) {return 1;}*p = 20;free(p);p = NULL;
4.2 对动态开辟的空间的越界访问
只开辟了40个字节的空间,这里却访问到了数组下标为10的元素,这就造成了动态开辟的空间的越界访问。
int* p = malloc(40);
if (p == NULL)
{printf("%s\n",strerror(errno));return 1;
}
for (int i = 0; i <= 10; i++)
{p[i] = i;p++;
}free(p);
p = NULL;
4.3 对非动态开辟的空间进行free释放
这个问题我们之前探讨过,例如在栈空间开辟的空间是不能用free进行释放的。
int i = 10;
int* p = &i;
free(p);
p = NULL;
4.4 使用free释放动态内存开辟的一部分
循环内部让指针不断加1,这就会导致p会指向这块开辟内存的最后面,这就会导致无法释放这块起始地址的空间。
int* p = malloc(40);
if (p == NULL)
{printf("%s\n",strerror(errno));return 1;
}
for (int i = 0; i <= 10; i++)
{*p = i;p++;
}free(p);
p = NULL;
4.5 对同一块内存空间的多次释放
平时free之后及时地将p置为空,那么后面如果对p进行释放,也不会造成太大影响。
int* p = malloc(40);
...
free(p);
...
free(p);
p = NULL;
4.6 动态内存忘记释放(内存泄漏)
看下面的代码是否有逻辑缺陷,下面的free有机会没有被执行,所以一定会导致内存泄露。
int* p = (int*)malloc(100);
int flag = 0;
scanf("%d",&flag);
if (flag == 5) {return;
}
free(p);
p = NULL;
5. 经典面试题
5.1 看代码说输出结果
首先调用getmemory,p指向了一百个字节的空间,出函数之后p直接销毁,导致内存泄露;
此时的str仍然是空指针,调用strcpy的时候如果传入空指针,之前我们解析了strcpy的内部实现,需要用到解引用,空指针解引用一定会导致程序崩溃。
如何改正,让程序正确运行呢?本质上就是想让str能够改变,所以需要传str的地址,str本身是指针,指针的地址需要用二级指针来接收,在函数内部进行解引用来获得地址,这样一来在函数内部就能改变函数外部的变量。 当然开辟了空间记得要释放。
5.2 指出下面程序的问题
int* f1(void)
{int x = 10;return (&x);
}
这段代码是一个函数,x是栈空间的数据,函数执行完毕后会销毁,但是这里把x的地址传出去了,这就导致了内存泄露,该地址的内存数据已经被回收,但是地址仍然被记录下来了,此时接收该地址的指针就是野指针。
int* f2(void)
{int* ptr;*ptr = 10;return ptr;
}
这段代码也是野指针的问题,和上面的代码大同小异不再赘述。