【逐步剖C】-第十一章-动态内存管理

一、为什么要有动态内存管理

从我们平常的学习经历来看,所开辟的数组一般都为固定长度大小的数组;但从很多现实需求来看需要我们开辟一个长度“可变”的数组,即这个数组的大小不能在建立数组时就指定,需要根据某个变量作为标准。

如比较常见的就是一些编程题中,输入一个变量n来作为数组的长度等(PS:虽C99支持变长数组,但我们这里主要讨论数组共性的标准);可能还有一种情况就是在往数组中放数据时,由于一开始空间大小指定不合适,出现了空间不足的情况,此时就需要进行扩容”操作。

由此一来,动态内存管理应运而生。

二、动态内存函数

1、malloc和free

(1)malloc函数介绍

  • 函数的声明为
void* malloc (size_t size);
  • 函数的作用
    这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针,通过相应类型的指针变量接收返回的指针后,即可通过该指针变量使用已开辟的内存空间
  • 函数的特性
    如果开辟成功,则返回一个指向开辟好空间的指针。
    如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
    返回值的类型是void* ,因为malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
    如果参数 size为0,malloc的行为是标准是未定义的,取决于编译器

使用示例:

    int n = 0;scanf("%d", &n);int* a = (int*)malloc(sizeof(int) * n);//开辟大小为n个整型的空间if (a != NULL)	//判断空间是否开辟成功{a[0] = 1;	//对空间进行使用//...}

可以看到,我们在使用时需要通过强制类型转换“告诉”编译器我们开辟空间的类型(返回指针的类型)(PS:其实空间并没有所谓的“类型”的概念,仅是通过指针的不同类型而有不同的看待空间的视角

(2)free函数介绍

  • 函数声明为
void free (void* ptr);
  • 函数的作用
    释放动态开辟的内存
  • 函数的特性
    如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
    如果是动态开辟的,则该函数会将ptr指向空间的使用权进行归还,其中有两部分,大部分归还给操作系统(真正释放),另一部分未归还操作系统的已释放内存被恢复到空闲池(free pool)中,并可再次进行分配
    如果参数 ptrNULL指针,则函数什么事都不做。

使用示例:

    int n = 0;scanf("%d", &n);int* a = (int*)malloc(sizeof(int) * n);//开辟大小为n个整型的空间if (a != NULL)	//判断空间是否开辟成功{a[0] = 1;	//对空间进行使用//...}free(a);	//释放动态开辟的内存

使用注意事项
从函数的特性中我们可以看出,在free一块动态开辟的内存后,那块空间按理来说已经不能再访问了,虽然的确可以故意去访问,当访问到的是空闲池中的内存时可能不会有什么大问题,但若访问到了操作系统的内存程序就会崩溃;又因为free仅负责将动态开辟的空间的使用权进行归还,并不对用于接收这块空间起始地址的指针进行处理(示例中a再free之后仍指向那块空间),故为了内存访问的安全,在free之后需对相应的指针进行置空操作。(示例中最后还需加上a = NULL

2、calloc

(1) 函数的声明为

void* calloc (size_t num, size_t size);

(2)函数的作用
作用和malloc一样,唯一不同的是它可以将动态开辟好的空间进行初始化,即将开辟好的num个空间的值都初始化为0。
(3)函数的特性
同malloc,但多了一个初始化功能。

使用示例:

    int n = 0;scanf("%d", &n);int* a = (int*)calloc(n,sizeof(int));//开辟大小为n个整型的空间

3、realloc

(1) 函数的声明为:

void* realloc (void* ptr, size_t size);

(2)函数的作用
对动态开辟内存大小进行调整,一般用于给已开辟的空间进行扩容。

(3)函数的特性
参数ptr是要调整的内存地址,size是调整之后的新大小(一般即为新空间的大小为原空间大小+需要扩展的空间的大小)。

若参数size为0或空间开辟失败时,函数返回NULL

若参数ptrNULL,该函数等价于malloc,如:

int*p = NULL;
p = realloc(ptr, 1000);

空间开辟成功会分为两种情况

  • 原开辟空间后有足够空间可以扩容,那么函数直接在原有内存之后直接追加空间,原来空间的数据不发生变化,返回旧的起始地址(ptr的值)

  • 在这里插入图片描述

  • 原开辟空间空间无法满足扩容需求,函数会先在堆空间上另找一个新的合适大小的连续空间来使用,接着把原空间的数据拷贝至新空间前面的位置,并把原空间释放,最后返回一个新空间的内存地址
    在这里插入图片描述

从特性中我们得到使用时的重要一点:
我们在对原空间进行扩容时,需用一个同类型的新指针来接收扩容之后返回的指针,以防分配失败而造成原有数据的丢失,如:

int *ptr = (int*)malloc(100);
ptr = (int*)realloc(ptr, 1000);//如上代码中,若realloc分配失败放回空指针,ptr所指向的原空间数据丢失
//正确应写为:int *ptr = (int*)malloc(100);
int *tmp = (int*)realloc(ptr, 1000);
if(tmp != NULL)
{ptr = tmp;
}

补充一点
realloc函数一般仅用于对内存进行扩展,也就是扩容;很少会用于缩容,并且缩容会存在一些问题,并且可能不能达到我们预期的效果。如下通过VS2022下的调试说明一下:
用于调试的代码:

int main()
{int* p = (int*)malloc(10 * sizeof(int));p[5] = 1;p = (int*)realloc(p, 5*sizeof(int));p[5] = 2;return 0;
}

在这里插入图片描述
分配10个整型的空间后,将第6个整型空间的值改为1,没问题;接下来将p的空间缩减为5个整型空间:
在这里插入图片描述
可以看到,后5个整型空间好像确实是回收给系统了,那么接下来执行a[5] = 2;应为越界访问的行为,系统按理来说会报错,但实际是:
在这里插入图片描述
仍完成了对原空间第6个整型空间的值的更改,这就与我们的预期效果大相径庭了。故一般不用realloc来进行缩容。

三、常见动态内存错误

1、对NULL指针的解引用操作

若对动态开辟返回的指针不做检查就可能发生,如:

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

2、对动态开辟的内存空间进行了越界访问

和数组越界访问的问题类型:

int i = 0;
int *p = (int *)malloc(10*sizeof(int));
if(NULL == p)
{exit(-1);
}
for(i=0; i<=10; i++)
{*(p+i) = i;		//当i是10的时候越界访问
}
free(p);

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

int a = 10;
int *p = &a;
free(p);

如上代码运行后系统崩溃:
在这里插入图片描述

4、 使用free释放一块动态开辟内存的一部分

int *p = (int *)malloc(100);
p++;		//p不再指向动态内存的起始位置
free(p);	

如上代码运行后系统崩溃:
在这里插入图片描述

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

int *p = (int *)malloc(100);
free(p);
free(p);//重复释放

如上代码运行后系统崩溃:
在这里插入图片描述

6、使用完动态开辟的内存后忘记释放

若使用完动态开辟的空间后没有通过free函数进行内存释放,就会造成恐怖的内存泄露问题,体现在我们的程序中可能没什么大问题(程序会结束或关闭,顺带着内存就会回收);但若体现在一些长时间不停机服务器中,就会造成服务器越用越卡直至死机的严重后果。

四、关于内存管理的经典题目

了解完动态内存管理的基础知识后,可以看看一些关于内存管理的经典题目来趁热打铁,请看:
1、运行Test 函数的结果是

void GetMemory(char *p)
{p = (char *)malloc(100);
}
void Test(void)
{char *str = NULL;GetMemory(str);strcpy(str, "hello world");printf(str);
}

结果
程序会崩溃。
原因
解引用了空指针。在GetMemory函数中动态分配内存后返回的指针赋值给了p,但由于形参对实参的临时拷贝,故改变了p并不影响str,故在GetMemory函数调用结束后,str的值仍为NULL,在进行strcpy时发生了错误。

2、运行Test 函数的结果是

char *GetMemory(void)
{char p[] = "hello world";return p;
}
void Test(void)
{char *str = NULL;str = GetMemory();printf(str);
}

结果
程序会打印“烫烫烫烫烫烫…”的乱码
原因
返回栈空间地址问题。GetMemory中p为局部变量,其在出了GetMemory函数作用域后会销毁,销毁后其原指向的空间的值就为随机值,也就是说用str接收的指针所指向的空间随机值,故再以打印字符串的方式去打印str的内容时就会乱码。

3、运行Test 函数的结果是

void GetMemory(char **p, int num)
{*p = (char *)malloc(num);
}
void Test(void)
{char *str = NULL;GetMemory(&str, 100);strcpy(str, "hello");printf(str);
}

结果
正常输出hello
原因
其实这才是相对于题目1的正确写法,由于实参本身就是一个指针,故需要一个二级指针来实现改变形参而改变实参

4、运行Test 函数的结果是

void Test(void)
{char *str = (char *) malloc(100);strcpy(str, "hello");free(str);if(str != NULL){strcpy(str, "world");printf(str);}
}

结果
看似正常输出了world
原因
在介绍free函数时有说到,free仅将动态开辟的空间的使用权还给系统,但并不改变用于“接收”(指向)这块空间的指针变量的值。也就是说,代码中的str在free后的值仍未原空间的起始地址,不为空,但此时对原空间已没有使用权,故在进行strcpy时本质上已经是非法访问了内存空间,只是可能访问到的是空闲池而程序没有崩溃。

五、C/C++内存区域划分

C/C++内存区域主要划分为如下几个区域:
1、栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。

2、 堆区(heap):一般由程序员申请分配与释放, 若程序员不释放,程序结束时可能由操作系统回收 。

3、数据段(静态区)(static):存放全局变量、静态数据。程序结束后由系统释放。

4、代码段:存放函数体(类成员函数和全局函数)的二进制代码

六、拓展:柔性数组

1、定义

(1)概念:C99 中,结构中的最后一个元素允许是未知大小的数组,该数组就称为柔性数组成员
(2)定义方式

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

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

2、柔性数组的特点

其实包含柔性数组成员的结构体的特点,主要有如下三点:
(1)结构中的柔性数组成员前面必须至少一个其他成员
(2)sizeof 返回的这种结构大小不包括柔性数组的内存
(3)包含柔性数组成员的结构用malloc函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小

3、柔性数组的使用及优势

(1)柔性数组的使用
柔性数组的使用主要在于对需要开辟的空间大小的把握,空间正确开辟后,和正常作为结构体成员的数组一样使用即可。下面是使用示例,请看:

type_a *p = (type_a*)malloc(sizeof(type_a)+10*sizeof(int));
//这里的柔性数组成员相当于获得了10个连续的整型空间//和正常数组一样使用即可
for(int i=0; i<10; i++)
{p->a[i] = i;
}free(p);

(2)与指针成员相比的优势
如上的结构体type_a 也可设计为:

typedef struct st_type
{int i;int* p_a;
}type_a;

这样在分配和释放空间时就会相对麻烦一些:

type_a *p = (type_a *)malloc(sizeof(type_a));
//先为整个结构体分配空间p->p_a = (int *)malloc(p->10 *sizeof(int));
//才能为里面的指针成员p分配空间for(int i=0; i<10; i++)
{p->p_a[i] = i;
}//要先释放指针成员的空间
free(p->p_a);
p->p_a = NULL;
//再释放整个结构体的空间
free(p);
p = NULL;

相比之下,我们可以得到柔性数组成员的两个优势:
(1)方便内存释放
有柔性数组成员的结构体在释放空间时仅需将为整个结构体分配的内存释放即可;而有指针成员的结构体在释放空间时需先释放指针为指针成员开辟的空间,才能释放为整个结构体开的空间,否则就会造成内存泄漏。
此时若是我们自己写的代码可能知道要先释放结构体指针成员的空间,但如果写的代码是给别人用时,用户可能就想着释放结构体就行,而不再会去在意结构体里有什么。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉,降低了内存泄漏的风险。
(2)一定程度上提高了内存访问速度
柔性数组成员的地址空间相对于整个结构体成员的空间地址是连续的,而指针成员则是碎片化的;如下示意图:

  • 柔性数组成员
    在这里插入图片描述
  • 指针成员

在这里插入图片描述

连续的内存有益于提高访问速度。

本章完。

看完觉得有觉得帮助的话不妨点赞收藏鼓励一下,有疑问或看不懂的地方或有可优化的部分还恳请朋友们留个评论,多多指点,谢谢朋友们!🌹🌹🌹

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

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

相关文章

分词.join 保存txt

要求 分词.join 保存txt 第1种方法 分词.join 保存txt input多行文本 /storage/emulated/0/数据中心/txt没有就新建为什么会想到这么做 1. 是因为有分词文件&#x1f4c4;要处理 2. 对各种词语和线索进行分类 3. 解释一下生活中不常见的现象&#xff0c;但是深刻的符合社会…

Inno Setup新手使用教程

1.编写脚本.iss文件 2.使用Inno Setup打开脚本 3.点击运行 4.打包好的文件在output文件夹下 注&#xff1a;运行不通过可能是文件不存在或者路径错误 推荐一个零声学院项目课&#xff0c;个人觉得老师讲得不错&#xff0c;分享给大家&#xff1a; 零声白金学习卡&#xff08;含…

PsychoPy Coder 心理学实验 斯特鲁普效应

选题&#xff1a;斯特鲁普效应实验 选题来源&#xff1a;你知道的「有趣的心理学实验」有哪些&#xff1f; - 知乎 (zhihu.com) 测试目标&#xff1a;探索斯特鲁普效应&#xff0c;即被试在判断文字颜色时&#xff0c;当文字的颜色与其所表示的颜色名称不一致时&#xff0c;是…

博途1200/1500 ALT指令

SMART PLC的ALT指令实现代码,请查看下面文章博客 SMART PLC如何构造ALT指令_smart200类似alt指令-CSDN博客单按钮启停这些老生常谈的问题,很多人感兴趣。这篇博文讨论下不同的实现方法,希望对大家有所帮助。指令虽然简单,但是在编程的时候合理使用对我们高效率编程帮助还是…

C/S架构学习之TCP的三次握手和四次挥手

TCP的三次握手&#xff1a;一定由客户端主动发起的&#xff0c;发生在建立连接的过程中。此过程发生在客户端的connect()函数和服务器的accept()函数之间。第一次握手&#xff1a;客户端向服务器发送一个带有SYN标志的数据包&#xff0c;表示客户端请求建立连接。并且客户端会选…

GO 中优雅编码和降低圈复杂度

本次主要是聊聊关于使用接口抽象和降低圈复杂度的方式 工作中&#xff0c;难免会遇到老项目老代码&#xff0c;不仅仅需要我们维护&#xff0c;可能还需要我们在原来的垃圾代码上进行新增功能或者是进行优化调整 例如 现有的老代码中关于用户系统这一块就已经经是摇摇欲坠&a…

python修改unittestreport中的用例条数

背景: 自动化框架中使用yaml文件作为数据配置&#xff0c;使用ddt作为数据驱动来运行测试用例&#xff0c;由于测试用例都是基于场景去编写&#xff0c;目前都是一个测试类算是一条测试用例&#xff0c;但基于测试报告里面一个类运行的测试方法有多个&#xff0c;因此统计的测试…

MATLAB 函数签名器

文章目录 MATLAB 函数签名器注释规范模板参数类型 kind数据格式 type选项的支持 使用可执行程序封装为m函数程序输出 编译待办事项推荐阅读附录 MATLAB 函数签名器 MATLAB 函数签名器 (FUNCSIGN) &#xff0c;在规范注释格式的基础上为函数文件或类文件自动生成函数签名&#…

专题一:双指针【优选算法】

双指针应用场景&#xff1a; 数组划分、数组分块 目录 一、移动0 二、复写0 从后向前 三、快乐数 链表带环 四、盛水最多的容器 单调性双指针 五、有效三角形个数 单调性双指针 六、和为s的两个数字 七、三数之和 细节多 需再练 一、移动0 class Solution { public:void move…

使用Jest测试Cesium源码

使用Jest测试Cesium源码 介绍环境Cesium安装Jest安装Jest模块包安装babel安装Jest的VSC插件 测试例子小结 介绍 在使用Cesium时&#xff0c;我们常常需要编写自己的业务代码&#xff0c;其中需要引用Cesium的源码&#xff0c;这样方便调试。此外&#xff0c;目前代码中直接使用…

ChatGPT付费创作系统V2.3.4独立版 +WEB端+ H5端 + 小程序最新前端

人类小徐提供的GPT付费体验系统最新版系统是一款基于ThinkPHP框架开发的AI问答小程序&#xff0c;是基于国外很火的ChatGPT进行开发的Ai智能问答小程序。当前全民热议ChatGPT&#xff0c;流量超级大&#xff0c;引流不要太简单&#xff01;一键下单即可拥有自己的GPT&#xff0…

C++——list(2)

作者&#xff1a;几冬雪来 时间&#xff1a;2023年9月28日 内容&#xff1a;C——list内容讲解 目录 前言&#xff1a; list的const迭代器&#xff1a; const的iterator&#xff1a; const迭代器&#xff1a; operator->: 拷贝构造&#xff1a; 迭代器接口补充&…

通过融合UGV的地图信息和IMU的惯性测量数据,实现对车辆精确位置和运动状态的估计和跟踪研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

六、互联网技术——数据存储

文章目录 一、存储系统层次结构二、按照重要性分类三、磁盘阵列RAID三、RAID基础四、磁盘阵列分级五、数据备份与恢复六、容灾与灾难恢复 一、存储系统层次结构 常见的三层存储体系结构如下图所示&#xff0c;分为高速缓冲存储器、主存储器和外存储器。 二、按照重要性分类 …

VBA技术资料MF66:使用代码插入行或列

我给VBA的定义&#xff1a;VBA是个人小型自动化处理的有效工具。利用好了&#xff0c;可以大大提高自己的工作效率&#xff0c;而且可以提高数据的准确度。我的教程一共九套&#xff0c;分为初级、中级、高级三大部分。是对VBA的系统讲解&#xff0c;从简单的入门&#xff0c;到…

十二、同步互斥与通信

1、概述 (1)可以把多任务系统当做一个团队&#xff0c;里面的每一个任务就相当于团队中的一个人。团队成员之间要协调工作进度(同步)、争用会议室(互斥)、沟通(通信)。多任务系统中所涉及的概念&#xff0c;都可以在现实生活中找到例子。 (2)各类RTOS都会涉及这些概念&#x…

小程序入门笔记(一) 黑马程序员前端微信小程序开发教程

微信小程序基本介绍 小程序和普通网页有以下几点区别&#xff1a; 运行环境&#xff1a;小程序可以在手机的操作系统上直接运行&#xff0c;如微信、支付宝等&#xff1b;而普通网页需要在浏览器中打开才能运行。 开发技术&#xff1a;小程序采用前端技术进行开发&#xff0c;…

XC5013 马达驱动和充电集成一体的控制芯片 一档输出芯片

XC5013 是一款应用于马达驱动或 LED 驱动的控制芯片&#xff0c;集成了锂电池充电管理系统&#xff0c;设定一档高电平输 出&#xff0c;并带有对不同状态的 LED 指示功能。 XC5013 集成了涓流充电、恒流充电和恒压充电全过程的充电方式&#xff0c;浮充电压精度在全温度范…

正点原子嵌入式linux驱动开发——TF-A初探

上一篇笔记中&#xff0c;正点原子的文档简单讲解了一下什么是TF-A&#xff0c;并且也学习了如何编译TF-A。但是TF-A是如何运行的&#xff0c;它的一个运行流程并未涉及。TF-A的详细运行过程是很复杂的&#xff0c;涉及到很多ARM处理器底层知识&#xff0c;所以这一篇笔记的内容…

大促节奏:速卖通黑五接力双十一,如何打造产品权重瓜分活动流量

双十一和黑五作为一种独特的消费文化现象&#xff0c;已经逐渐成为了消费领域中的一块“金字招牌”。无论是消费者还是商家&#xff0c;都非常期待这一天的到来&#xff0c;因为它不仅代表着购物的欲望和刺激&#xff0c;更重要的是&#xff0c;双十一和黑五已经成为了一种全新…