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

news2024/10/6 16:23:11

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

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

如比较常见的就是一些编程题中,输入一个变量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.coloradmin.cn/o/1062474.html

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

相关文章

小样本学习——匹配网络

目录 匹配网络 &#xff08;1&#xff09;简单介绍&#xff1a; &#xff08;2&#xff09;专业术语 &#xff08;3&#xff09;主要思想 &#xff08;4&#xff09;训练过程 问题 回答 MANN 匹配网络 &#xff08;1&#xff09;简单介绍&#xff1a; Matching netwo…

【C++设计模式之装饰模式:结构型】分析及示例

装饰模式&#xff08;Decorator Pattern&#xff09;是一种结构型设计模式&#xff0c;它允许在运行时动态地给一个对象添加额外的行为。 描述 装饰模式通过创建一个包装器&#xff08;Wrapper&#xff09;来包裹原始对象&#xff0c;并在原始对象的行为前后添加额外的功能。…

JAVA学习(5)-全网最详细~

&#x1f308;write in front&#x1f308; &#x1f9f8;大家好&#xff0c;我是Aileen&#x1f9f8;.希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流. &#x1f194;本文由Aileen_0v0&#x1f9f8; 原创 CSDN首发&#x1f412; 如…

git提交代码的流程

1.拉取代码 当你进入了一家公司就需要拉去公司的代码进行开发,此时你的项目小组长会给你个地址拉代码, git clone 公司项目的地址 此时如果不使用了这个方式拉去代码,拉去的是master分支上的代码,但是很多数的情况下&#xff0c;公司的项目可能会在其它的分支上,因此到公…

XXL-JOB源码梳理——一文理清XXL-JOB实现方案

分布式定时任务调度系统 流程分析 一个分布式定时任务&#xff0c;需要具备有以下几点功能&#xff1a; 核心功能&#xff1a;定时调度、任务管理、可观测日志高可用&#xff1a;集群、分片、失败处理高性能&#xff1a;分布式锁扩展功能&#xff1a;可视化运维、多语言、任…

微信小程序代驾系统源码(含未编译前端,二开无忧) v2.5

简介&#xff1a; 如今有越来越多的人在网上做代驾&#xff0c;打造一个代驾平台&#xff0c;既可以让司机增加一笔额外的收入&#xff0c;也解决了车主酒后不能开发的问题&#xff0c;代驾系统基于微信小程序开发的代驾系统支持一键下单叫代驾&#xff0c;支持代驾人员保证金…

【15】c++设计模式——>抽象工厂模式

在海贼世界中&#xff0c;位于水之都的弗兰奇一家是由铁人弗兰奇所领导的以拆船为职业的家族&#xff0c;当然了他们的逆向工程做的也很好&#xff0c;会拆船必然会造船。船是海贼们出海所必备的海上交通工具&#xff0c;它由很多的零件组成&#xff0c;从宏观上看它有这么几个…

数据结构--》探索数据结构中的字符串结构与算法

本文将带你深入了解串的基本概念、表示方法以及串操作的常见算法。通过深入理解串的相关概念和操作&#xff0c;我们将能够更好地应用它们来解决算法问题。 无论你是初学者还是进阶者&#xff0c;本文将为你提供简单易懂、实用可行的知识点&#xff0c;帮助你更好地掌握串在数据…

【云笔记篇】Microsoft OneNote笔记插件推荐OneMore

【云笔记篇】Microsoft OneNote笔记插件推荐OneMore OneMore插件是一款非常强大&#xff0c;多达一百多个扩展功能的OneNote笔记插件&#xff0c;而且免费开源&#xff0c;不断更新的优秀插件—【蘇小沐】 1、实验 【OneMore官网&#xff1a;OneMore - a OneNote add-in (on…

使用idea 中的rest 将 git 合并部分分支代码到主分支

需求&#xff1a;当要将dev的分支中的部分代码合并到test分支时&#xff0c;又不想把dev的全部代码合并到test分支 例如dev分支已经提交了 demo1到4&#xff0c;到想把demo1-3的代码合并到test分支&#xff0c;demo4暂时不合并 可以使用idea的reset 功能满足以上需求 1首先切…

Activity之间数据回传【Android、activity回传、结合实例】

任务要求 在Android应用中&#xff0c;有时需要从一个Activity向另一个Activity传递数据&#xff0c;并在第二个Activity处理后将结果传递回第一个Activity。 这种情况下&#xff0c;我们可以使用startActivityForResult()和onActivityResult()方法来实现数据回传。 实现步骤…

Day-07 修改 Nginx 配置文件

至此&#xff1a; 简单的 Docker 安装 Nginx并启动算是成功了! ps: 如何修改 Nginx的配置、更改nginx 的资源文件&#xff1f; eg&#xff1a; 1、可以将容器中的目录和本机目录做映射。 2、达到修改本机目录文件就影响到容器中的文件。 1.本机创建实例文件夹 新建目录&#x…

图像拼接后丢失数据,转tiff报错rasterfile failed: an unknown

图像拼接后丢失数据 不仅是数据丢失了&#xff0c;还有个未知原因报错 部分数据存在值不存在的情况 原因 处理遥感数据很容易&#xff0c;磁盘爆满了 解决方案 清理一些无用数据&#xff0c;准备买个2T的外接硬盘用着了。 然后重新做处理

获取沪深300的所有个股列表

脚本&#xff1a; import requests from bs4 import BeautifulSoupurl "https://q.stock.sohu.com/cn/bk_4444.shtml" response requests.get(url) soup BeautifulSoup(response.text, "html.parser")# 找到包含class为e1的元素 elements soup.find_a…

计算机毕设 大数据房价预测分析与可视

文章目录 0 前言1 课题背景2 导入相关的数据 3 观察各项主要特征与房屋售价的关系4 最后 0 前言 &#x1f525; 这两年开始毕业设计和毕业答辩的要求和难度不断提升&#xff0c;传统的毕设题目缺少创新和亮点&#xff0c;往往达不到毕业答辩的要求&#xff0c;这两年不断有学弟…

战火使命攻略,高级回响怎么出现

《战火使命》中&#xff0c;高级回响材料是每个玩家追求的梦想&#xff0c;因为它们不仅能够提升角色的实力&#xff0c;还能让你在战场上独具风采。本战火使命攻略将详细介绍三种获取高级回响的方法&#xff0c;助你在游戏中更加强大&#xff01; 关注【娱乐天梯】&#xff0c…

数据结构-图-基础知识

图 图的基本概念图的概念顶点和边有向图和无向图完全图有向完全图无向完全图 邻接顶点顶点的度路径和路径长度简单路径和回路子图生成树 连通图强连通图 图的存储结构邻接矩阵邻接表 图的遍历BFSDFS 图的基本概念 图的概念 &#x1f680;图是由顶点集合及顶点间关系组成的一种…

O(根号n/ln(根号n))时间复杂度内求n的所有因子

O&#xff08;&#xff09;复杂度内求n的所有因子&#xff0c;在2e9数量级比O&#xff08;&#xff09;快10倍左右 先用范围内的质数除n&#xff0c;求出n的分解质因数形式&#xff0c;然后爆搜求出n的所有因子&#xff0c; n范围内的质数大约有个&#xff0c;所以是这个时间…

Spring Framework 黑马程序员-学习笔记

5.spring-核心概念 IoC &#xff1a;控制反转 使用对象时&#xff08;如在service类中调用Dao层的对象&#xff0c;以便使用Dao类中的方法&#xff09;&#xff0c;本来是依靠new一个Dao层的对象来实现&#xff0c;而实现了Ioc思想的Spring为了解耦&#xff0c;将此过程改为&…