当前位置: 首页 > 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.lryc.cn/news/596812.html

相关文章:

  • 新手向:Idea的使用技巧
  • 10. isaacsim4.2教程-RTX Lidar 传感器
  • AWS Lambda IoT数据处理异常深度分析:从告警到根因的完整排查之路
  • Vue3 面试题及详细答案120道(61-75 )
  • Python学习:函数的使用
  • webrtc整体架构
  • LeetCode热题100--205
  • Visual Studio中部署PaddleOCRv5 (借助ncnn框架)
  • Flink 状态管理设计详解:StateBackend、State、RocksDB和Namespace
  • 【笔记】Handy Multi-Agent Tutorial 第三章: CAMEL框架简介及实践(实践部分)
  • Redis原理之分布式锁
  • PowerShell自动化核对AD与HR系统账户信息实战指南
  • IDEA202403 超好用设置【持续更新】
  • ZooKeeper在Hadoop中的协同应用:从NameNode选主到分布式锁实现
  • 天津大学陈亚楠教授团队 ACS AEM:焦耳热超快合成非平衡态能源材料——毫秒级制备与跨体系性能突破
  • 昨天去看了电科金仓的发布会,有点东西!
  • 从 Linux 将文件下载到 Windows 的几种实用方法
  • 【AI智能体】Dify 开发与集成MCP服务实战操作详解
  • 嵌入式学习之路
  • Python笔记之跨文件实例化、跨文件调用、导入库
  • 为什么本地ip记录成0.0.0.1
  • 基于Python flask的常用AI工具功能数据分析与可视化系统设计与实现,技术包括LSTM、SVM、朴素贝叶斯三种算法,echart可视化
  • 慢 SQL接口性能优化实战
  • Fast Frequency Estimation Algorithm by Least Squares Phase Unwrapping
  • USB4.0:开启高速数据传输的新时代
  • 当if else比较多时候应该怎么避免?
  • MCP与企业数据集成:ERP、CRM、数据仓库的统一接入
  • #Linux权限管理:从“Permission denied“到系统安全大师
  • uniapp自定义圆形勾选框和全选框
  • iOS 抓包工具有哪些?2025实用指南与场景推荐