你不知道的C语言知识(第八期:动态内存管理)

在这里插入图片描述
本期介绍🍖
主要介绍:C语言中一些大家熟知知识点中的盲区,这是第八期,主讲动态内存管理。


文章目录

  • 1. 为什么会存在动态内存
  • 2. 动态内存管理库函数
    • 2.1 malloc函数
    • 2.2 calloc函数
    • 2.3 realloc函数
    • 2.4 free函数
  • 3. 内存泄漏&内存池
  • 4. 常见的动态内存错误
    • 4.1 对NULL指针解引用操作
    • 4.2 对动态开辟空间越界访问
    • 4.3 对非动态开辟内存使用free释放
    • 4.4 使用free释放动态开辟内存的一部分
    • 4.5 对同一块动态内存多次释放
    • 4.6 动态开辟内存忘记释放(内存泄漏)
  • 5. 柔性数组
    • 5.1 什么是柔性数组
    • 5.2 柔性数组的特点
    • 5.3 柔性数组的应用
  • 6. C/C++中程序内存区域划分


1. 为什么会存在动态内存

  在学习动态内存管理前,已知的两种开辟内存空间的方式:1.int a; (类型+变量名的方式),2.int arr[10];(数组的开辟方式)。但这两种开辟方式有局限性,一旦开辟就无法再更改开辟空间的大小了。
  举个例子:创建有一个有100个元素的结构体数组,数组每个元素都是一个人的信息。假如仅需要存放3个人的信息,对于这个结构体数组来说,剩余的97个空间不就浪费了嘛。同理假如需要存放120个人的信息,对于该数组来说是放不下的,又不能对其大小进行修改。
  可见这两种对于空间的开辟方式是不灵活的,那么是否存在能自主管理空间的内存开辟方式?有了这种开辟方式,开辟的内存空间能够想大就大想小就小。C语言提供了这种方式:动态内存管理。在C语言中是通过下面这些库函数函数来实现的,malloc()calloc()realloc()free()。值得注意的是,在使用这些库函数前需要引用头文件<stdlib.h>


2. 动态内存管理库函数

2.1 malloc函数

  malloc()是一个动态内存开辟函数。这个函数会向内存申请一块大小为size的连续空间(单位:字节),并返回一个指向该空间起始位置处的地址(指针的类为void*)。函数声明如下所示:

	void* malloc(size_t size);

  值得注意,如果参数size0,这种malloc()行为是标准未定义的,取决于编译器。如果malloc()开辟内存空间失败,会直接返回NULL,因此malloc()的返回值一定要做检查。举例如下所示:

#include<stdio.h>
#include<stdlib.h>
int main()
{char* pc = (char*)malloc(sizeof(char) * 10);if(pc == NULL){perror("malloc");return 1;}//注意:在C语言中有一个习惯,return 0;表示正常返回,return 1;表示异常返回。//执行下面的代码//...return 0;
}

2.2 calloc函数

  calloc()是一个动态内存开辟函数。这个函数会向内存申请一块大小为num * size的连续空间(单位:字节),然后把这块申请空间的每个字节初始化为0,最后返回一个指向该空间起始位置处的地址(指针的类为void*)。函数声明如下所示:

	void* calloc(size_t num, size_t size);

  与malloc()函数相同的是,calloc()函数如果参数num*size的结果为0,这种calloc()行为是标准未定义的,取决于编译器;如果calloc()开辟内存空间失败,会直接返回NULL,因此calloc()的返回值一定要做检查。与malloc()函数不同的是,calloc()函数申请完空间后,会将空间全部初始化为0再返回。举例验证如下:

#include<stdio.h>
#include<stdlib.h>
int main()
{char* pc = (char*)calloc(20, sizeof(char));if (pc == NULL){perror("malloc");return 1;}//执行下面的代码//...return 0;
}

在这里插入图片描述


2.3 realloc函数

  realloc()函数是一个能够调整动态开辟内存大小的函数,也可以动态开辟空间,只是参数需要为NULL。有时大家会发现过去申请的空间太小了,有时又会觉得申请的空间太大了。为了能够更有效的使用内存,就需要对申请空间的大小做灵活的调整,这时就需要用realloc()函数了。函数类型声明如下:

	void* realloc(void* ptr, size_t size);
  1. ptr:指向将要被调整大小的空间
  2. size:调整后新的大小(单位:字节)

  realloc()函数调整内存空间时存在两种情况:

  1. 原有空间后有足够大的空间

  在原空间的基础上直接向后追加空间,原空间内的数据不发生任何变化。调整前与调整后动态开辟内存的起始位置不发生任何变化,如下图所示:
在这里插入图片描述

  1. 原有空间后没有足够大的空间

  向后寻找一个能够存放的下调整后大小的连续空间,然后将原空间中的数据拷贝到新空间中,最后将新开辟空间的起始地址返回。值得注意,realloc在开辟完新空间后,会自动释放掉原先的那块空间,不需要手动释放。如下图所示:

在这里插入图片描述
  值得注意,若realloc()函数调整大小成功,则会返回调整后内存空间起始位置的地址(类型为void*);若调整失败,直接返回NULL。如果直接用维护原空间的指针ptr来接收调整后新空间的地址,必然会存在一种情况:当调整失败后,指向原空间的指针ptr被赋值为NULL(也就是说再也找不到原先那块动态开辟的内存空间了,也释放不掉那块空间,导致内存泄漏)。
  所以一般使用realloc()函数,会创建一个中间变量来接收函数的返回值。然后对其进行判断,如果不为NULL再将其赋值给ptr。举例如下所示:

#include<stdio.h>
#include<stdlib.h>
int main()
{//动态开辟空间char* pc = (char*)malloc(20 * sizeof(char));if (pc == NULL){perror("malloc");return 1;}//调整空间大小char* ptr = (char*)realloc(pc, 10);if (ptr != NULL){pc = ptr;}//执行下面的代码//...return 0;
}

2.4 free函数

  free()是专门用来回收或者说释放,动态内存开辟的空间的函数。其参数ptr为那块动态开辟空间的起始地址。函数类型声明如下所示:

	void free( void* ptr );

  关于free()函数,值得注意以下三点:

  1. 使用free()释放ptr维护的那块空间时,free()是不会将ptr顺手置为NULL
#include<stdio.h>
#include<stdlib.h>
int main()
{char* pc = (char*)malloc(10 * sizeof(char));if (pc == NULL){perror("malloc");return 1;}//执行下面的代码//...free(pc);return 0;
}

在这里插入图片描述

  由上图可知,ptr指向的那块空间确实被释放了(也就是还给操作系统了),但是ptr仍然指向那个位置,故此时的ptr野指针。所有大家应该养成一个习惯,在free()释放完一块空间后,就将指针赋为NULL

  1. 如果参数ptr指向的空间不是动态开辟的,那free()函数的行为是未定义的
  1. 如果参数ptrNULL指针,则函数什么事都不做

3. 内存泄漏&内存池

  内存泄漏通俗点来说,就是你向内存申请了一块空间,用完后不还,别人想用还用不到,如果你一直不还,那么这块空间不就相当于不存在了,也就是理论上泄漏掉了。内存泄漏的影响:系统内存浪费、程序运行缓慢、甚至系统崩溃等严重后果。
  纠正一件事,如果malloc()申请空间后没有使用free()释放,这种情况并不是内存泄漏。因为当程序结束时,操作系统会自动回收那块空间。除非那个程序一直运行不停下来,那么这时该程序就算是内存泄漏了。代码如下所示:

#include<stdio.h>
#include<stdlib.h>int main()
{int* ptr = (int*)malloc(sizeof(int) * 10);if (ptr == NULL){perror("malloc");return 1;}int i = 0;for (i = 0; i < 10; i++){*(ptr + i) = i + 1;}return 0;
}

  值得注意,动态内存的开辟是在堆区上的。而在堆区上开辟空间,空间之间是有间隙(内存碎片)。如果在内存中频繁的使用malloc()calloc()realloc开辟空间,就会使得内存中存在非常多的内存碎片,如果这些内存碎片在之后没能有效的利用的话,就会导致内存利用率和效率的下降。
  而想要解决上述问题,就需要引用一个新概念内存池那什么是内存池呢?内存池就是一种通过程序来维护内存空间的方法。是怎么实现的呢?首先我们会向内存申请一块相对来说能够满足我们当前需求的空间,然后程序内部用内存池的方式来维护这块空间,如此就不用应频繁的申请而打扰操作系统了,且解决了内存碎片的问题。


4. 常见的动态内存错误

4.1 对NULL指针解引用操作

  由于使用malloccalloc函数后没有对返回值进行判断所导致的。当malloccalloc向内存申请空间失败后,会直接返回NULL。如果没有对其进行判断直接上手使用,自然就会伴随着一定的风险。举个例子:

void test()
{int *p = (int *)malloc(INT_MAX/4);*p = 20;//如果p的值是NULL,就会有问题free(p);p = NULL;return 0;
}

4.2 对动态开辟空间越界访问

  之前在学习数组、指针时经常会编写一些越界访问的程序,而如今对于动态开辟出来的空间自然也会出现,由于操作不挡而导致的越界访问(所谓的越界访问,就是访问的过程中访问了不属于我们的空间)。举例如下:

void main()
{int i = 0;int *p = (int *)malloc(10*sizeof(int));if(NULL == p){printf("%s\n", strerror(errno));return 1;}for(i=0; i<=10; i++){*(p+i) = i;//当i是10的时候越界访问}free(p);p = NULL;return 0;
}

4.3 对非动态开辟内存使用free释放

  总是有些人喜欢一些奇奇怪怪的事,就譬如拿用来释放动态开辟空间的free函数去释放静态开辟的空间,操作系统会立即报错。如下所示:

int main()
{int arr[10] = { 0 };int* p = arr;free(p);//是否可行?return 0;
}

4.4 使用free释放动态开辟内存的一部分

  有时在使用维护动态开辟空间的指针来编写代码时,会在无意中移动这个指针,当想要用该指针释放动态内存时,该指针已然不指向该内存的起始位置,如此不就造成了部分内存释放了嘛。举个例子:

int main()
{int* ptr = (int*)malloc(40);if (ptr == NULL){printf("%s\n", strerror(errno));return 1;}int i = 0;for (i = 0; i < 5; i++){ptr++;}free(ptr);//ptr不再指向动态内存的起始位置ptr = NULL;return 0;
}

  对于动态内存的部分释放操作系统是不被允许的,free()必须是指向动态开辟空间的起始地址,这一点一定要注意了。


4.5 对同一块动态内存多次释放

int main()
{int* ptr = (int*)malloc(40);//...free(ptr);//...free(ptr);//重复释放return 0;
}

  在C语言中同一块动态内存是不允许被多次释放的,只能被释放一次,不然就报错。为了避免这种情况,我们其实可以在每次free释放完一块空间后立马将维护这块空间的指针置为NULL,这样下一次无意中再次对其释放就不会报错了,因为free(NULL);是不做任何操作的。


4.6 动态开辟内存忘记释放(内存泄漏)

  在不考虑程序结束时操作系统自动回收内存的情况下,下面的代码会导致内存泄漏。

void test()
{int* ptr = (int*)malloc(40);if (ptr == NULL){return ptr;}
}int main()
{test();return 0;
}

5. 柔性数组

5.1 什么是柔性数组

  柔性数组是一种动态可变的数组,通过结构体实现,在C99标准底下支持的一种语法。想要使用柔性数组需要满足3个条件:

  1. 柔性数组只能存在于结构体内,且必须是结构体最后一个成员
  2. 柔性数组成员前,至少存在一个其他成员
  3. 数组的大小未定义

  声明一个柔性数组,如下所示:

typedef struct st_type
{int i;int a[0];//柔性数组成员
}type_a;

  有些编译器会报错无法编译,可以改成如下形式:

typedef struct st_type
{int i;int a[];//柔性数组成员
}type_a;

5.2 柔性数组的特点

  1. 使用sizeof计算包含柔性数组的结构体的大小,得出的结果不包含柔性数组的大侠

在这里插入图片描述

  1. 包含柔性数组成员的结构体要用malloc函数来进行内存的动态分配,并且分配的内存应大于结构体的大小,以适应柔性数组的预期大小。如下所示:
#include<stdio.h>
#include<stdlib.h>typedef struct st_type
{int i;int a[0];//柔性数组成员
}type_a;int main()
{type_a* ptr = (type_a*)malloc(sizeof(type_a) + 100 * sizeof(int));if (ptr == NULL){perror("malloc");return 1;}ptr->i = 100;int i = 0;for (i = 0; i < 100; i++){ptr->a[i] = i + 1;}free(ptr);ptr = NULL;return 0;
}

  柔性数组就是以这种内存空间不断的变化,来使得整个数组拥有了动态的性能,某种意义上相当于该数组柔软可变的,所以称为柔性数组。内存布局如下所示:
在这里插入图片描述

&emps; 值得注意,若直接用这种包含柔性数组的结构体类型来创建变量,那么该结构变量相当于只有一个成员i。原因是该类型结构体的大小不包含柔性数组,故在像内存申请空间时,自然也不会获得柔性数组的空间,所以该结构体变量相当于就只有一个成员i


5.3 柔性数组的应用

  其实在实际应用中,完全可以用柔性数组结构体代替动态结构体,如下所示:

在这里插入图片描述

  可以看出,使用柔性数组实现的结构体,一整个都是在堆区上开辟的,而且是一块连续的空间。而用结构体中包含指向动态内存的指针的方法,是分为两块不同的区域来开辟的,结构体是在栈区上申请的,指针维护的那块空间是在堆区上申请的。相比较这两种写法,柔性数组的好处是:有利于访问速度。


6. C/C++中程序内存区域划分

在这里插入图片描述
  1. 内核空间:用户无法对这块空间进行读写,该空间是专门用来跑操作系统的。

  2. 栈区:在调用函数时,函数调用空间、函数内局部变量、函数参数的存储单元都是在栈区上创建的,函数调用结束时这些存储单元自动被释放。

  3. 堆区:一般由程序员自主的动态内存开辟和释放,若程序员不释放,程序结束时可能由OS回收。动态分配方式类似于链表。

  4. 数据段:就是之前所说的静态区,主要用于存放全局数据静态数据

  5. 代码段:用于存放代码经过编译链接后的二进制可执行程序只读常量

  实际上普通的的局部变量是由放栈区分配的空间,而栈区的特点是在上面创建的变量出了作用域就自动销毁。而对于用static修饰的变量存放到了数据段静态区),数据段的特点是在上面创建的变量,直到程序结束才会销毁,故生命周期长。而在堆区上开辟的空间特点是能够自主的调整其大小。


在这里插入图片描述

这份博客👍如果对你有帮助,给博主一个免费的点赞以示鼓励欢迎各位🔎点赞👍评论收藏⭐️,谢谢!!!
如果有什么疑问或不同的见解,欢迎评论区留言欧👀。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/3207.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

vue项目新打开一个tab页或者新窗口的方法

router.resolve&#xff1a;返回一个路由地址的规范化版本。同时包含一个包含任何现有 base 的 href 属性。默认情况下&#xff0c;用于 router.currentRoute 的 currentLocation 应该在特别高阶的用例下才会被覆写。 打开新标签页方法 const openNewTab ()> {// 打开新标…

Linux题目练习

1、配置网络&#xff1a;为网卡添加一个本网段IPV4地址&#xff0c;x.x.x.123 2、配置yum本地仓库&#xff0c;并完成traceroute命令的安装 3、用至少两种方法查看sshd服务的进程号 4、添加一块20G大小的磁盘&#xff0c;将该磁盘分为两个主分区&#xff0c;大小为1G、2G。将剩…

【Linux】动静态库:构建强大软件生态的基石

&#x1f308; 个人主页&#xff1a;Zfox_ &#x1f525; 系列专栏&#xff1a;Linux 目录 一&#xff1a;&#x1f525; 动静态库基本原理 二&#xff1a;&#x1f525; 认识动静态库 三&#xff1a;&#x1f525; 动静态库的优缺点 &#x1f98b; 静态库&#x1f98b; 动态库…

HRCE第四次实验

1.dns配置介绍 使用S/C架构 server --- bind(程序) --- 服务名称&#xff08;程序主进程&#xff09;named 开始实验前我们先进行主配置文件进行配置 vim /etc/named.conf进入主配置文件&#xff08;共分四部分&#xff09; 1.options{全局配置}&#xff08;多模块生效的配…

微服务设计模式 - 事件溯源模式(Event Sourcing Pattern)

微服务设计模式 - 事件溯源模式&#xff08;Event Sourcing Pattern&#xff09; 定义 事件溯源&#xff08;Event Sourcing&#xff09;是一种将所有状态更改保存为一系列事件的设计模式。每次系统状态发生变化时&#xff0c;都会生成一个事件&#xff0c;这些事件在事件存储…

Sketch下载安装,中文版在线免费用!

Sketch是一款轻便、高效的矢量设计工具&#xff0c;全球众多设计师借助它创造出了无数令人惊叹的作品。Sketch在下载安装方面&#xff0c;其矢量编辑、控件以及样式等功能颇具优势&#xff0c;不过&#xff0c;Sketch中文版即时设计在下载安装方面也毫不逊色。即时设计是一个一…

微服务之间的信息传递---OpenFeign拦截器

上篇我们已经实现了从网关传递信息到微服务。 新的问题是&#xff0c;微服务之间如何传递信息。 前面我们在公共模块中定义拦截器并保存用户信息到了线程变量。 但注意&#xff1a; 线程变量的作用域范围仅限于当前请求的线程。每个请求对应一个独立的线程变量&#xff0c;…

Nginx安装和配置

2.Nginx安装 2.1Nginx概述 2.1.1 Nginx介绍 Nginx&#xff08;engine x&#xff09;&#xff0c;2002 年开发&#xff0c;分为社区版和商业版&#xff08;nginx plus&#xff09; 2019 年3 月 15 日 F5 Networks 6.7 亿美元的价格收购 nginx是免费的、开源的、高性能的 HTTP…

2024全国铁路、高铁、地铁、有轨电车、窄轨铁路、单轨铁路、轻轨等矢量数据下载分享

数据是GIS的血液&#xff01; 我们在《全国地铁路线及站点SHP数据》一文中&#xff0c;分享了我国部分城市的地铁以及站点矢量数据。 现在又为你整理了2024全国铁路、高铁、地铁、有轨电车、窄轨铁路、单轨铁路、轻轨等矢量数据&#xff0c;你可以在文末查看该数据的下载领取…

git上传大文件的解决方案

大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。喜欢通过博客创作的方式对所学的…

DAY16|二叉树Part04|LeetCode: 513.找树左下角的值、112. 路径总和、106. 从中序与后序遍历序列构造二叉树

目录 LeetCode: 513.找树左下角的值 基本思路 C代码 LeetCode: 112. 路径总和、113.路径总和II LeetCode: 112. 路径总和 C代码 LeetCode: 113.路径总和II LeetCode: 106. 从中序与后序遍历序列构造二叉树 基本思路 C代码 LeetCode: 513.找树左下角的值 力扣代码链接…

【算力基础】GPU算力计算和其他相关基础(TFLOPS/TOPS/FP32/INT8...)

文章目录 :one: 算力的常见指标:two: 算力计算:three: 常用链接 &#x1f680; 本文主要是聚焦于深度学习领域的 GPU的算力估计&#xff0c;其他类型的硬件设备如CPU可以类比参考。 1️⃣ 算力的常见指标 算力衡量主要与运算速度和精度这两个指标有关。 &#x1f314;速度指…

云集电商:如何通过 OceanBase 实现降本 87.5%|OceanBase案例

云集电商&#xff0c;一家聚焦于社交电商的电商公司&#xff0c;专注于‘精选’理念&#xff0c;致力于为会员提供超高性价比的全品类精选商品&#xff0c;以“批发价”让亿万消费者买到质量可靠的商品。面对近年来外部环境的变化&#xff0c;公司对成本控制提出了更高要求&…

认定出现不安全行为并通过系统发出告警信息的名厨亮灶开源了。

简介 AI视频监控平台, 是一款功能强大且简单易用的实时算法视频监控系统。愿景在最底层打通各大芯片厂商相互间的壁垒&#xff0c;省去繁琐重复的适配流程&#xff0c;实现芯片、算法、应用的全流程组合&#xff0c;减少企业级应用约 95%的开发成本&#xff0c;在强大视频算法加…

yolov5快速复现

源码下载 进入github官网搜索yolov5(网址&#xff1a;https://github.com/ultralytics/yolov5)如下图红框所示&#xff1a; 进入界面如下图所示&#xff0c;点击右上角绿色Code&#xff0c;选择下载压缩包&#xff1a; 开发环境设置 本文使用云平台开发&#xff0c;系统为Li…

“再探构造函数”(2)

文章目录 一. 友元‘全局函数’作友元‘成员函数’作友元‘类‘作友元 二. 内部类三. 匿名对象四. 对象拷贝时的编译器优化分析调用时的顺序 一. 友元 何时会用到友元呢&#xff1f; 当想让&#xff08;类外面的某个函数/其它的类&#xff09;访问 某个类里面的(私有或保护的…

一周内从0到1开发一款 AR眼镜 相机应用?

目录 1. &#x1f4c2; 前言 2. &#x1f4a0; 任务拆分 2.1 产品需求拆分 2.2 开发工作拆分 3. &#x1f531; 开发实现 3.1 代码目录截图 3.2 app 模块 3.3 middleware 模块 3.4 portal 模块 4. ⚛️ 拍照与录像 4.1 前滑后滑统一处理 4.2 初始化 View 以及 Came…

Redis(2):内存模型

一、Redis内存统计 工欲善其事必先利其器&#xff0c;在说明Redis内存之前首先说明如何统计Redis使用内存的情况。 在客户端通过redis-cli连接服务器后&#xff08;后面如无特殊说明&#xff0c;客户端一律使用redis-cli&#xff09;&#xff0c;通过info命令可以查看内存使用情…

2024年CG作品网站推荐(精选篇)

ArtStation https://www.artstation.com GGAC https://www.ggac.com/

GraphRAG如何构建知识图谱Knowledge Graph

GraphRAG工作的第一步&#xff0c;是将输入的文档集合&#xff0c;按一定的策略拆分成一个一个chunks&#xff0c;然后解析每个chunks&#xff0c;将chunk中所关注的实体(entity)和关系(relation)解析出来&#xff0c;以此构建知识图谱。 那问题来了&#xff0c;GraphRAG是如何…