当前位置: 首页 > news >正文

【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;
}

        这段代码也是野指针的问题,和上面的代码大同小异不再赘述。

http://www.dtcms.com/a/294990.html

相关文章:

  • 力扣(LeetCode) ——轮转数组(C语言)
  • Unity UI的未来之路:从UGUI到UI Toolkit的架构演进与特性剖析(2)
  • 【Web APIs】JavaScript 节点操作 ⑦ ( 创建节点案例 | 网页评论功能 )
  • 旅游管理虚拟仿真实训室:重构实践教学新生态
  • 掌握Autofac:IOC容器实战指南
  • GaussDB view视图的用法
  • 分布式光伏发电项目简易故障录波装置介绍
  • [硬件电路-78]:模拟器件 - 从宏观到微观:高频信号下电路与光路的特性演变与挑战
  • Hexo - 免费搭建个人博客05 - 更新个人博客
  • GUI简介 - OpenExo
  • 回顾 Palantir:八年之旅的反思
  • ​​SBOM 软件供应链安全(转)
  • haproxy的七层代理
  • Day01_C++编程
  • Ollama(3)模型迁移和API使用
  • Modbus协议详解与c#应用
  • 重写 与 重载
  • pig cloud框架中引入websocket
  • nginx websocket 代理 断网后 再联网 不能连接
  • Windows下编译UTF8-CPP
  • 前端学习 5:DFT
  • 云效CI/CD教程(PHP项目)
  • 如何提升连带消费?从新零售“人-货-场”模型拆解
  • 220V降5V,输出100MA,为家电电器消费类产品提供电源WD5202L
  • OpenCV+Python
  • WebGIS 常用坐标系
  • 真的假的?CISP认证考试将全面推行线下机考?
  • 我考PostgreSQL中级专家证书二三事
  • ubuntu24.04 nvidia driver无效///重装驱动
  • MYOJ_8513:CSP初赛题单6:竞赛要求相关