【逐步剖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.mzph.cn/news/95350.shtml

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

相关文章

我的创作纪念日-第1024天

文章目录 一、机缘二、收获三、日常四、憧憬 一、机缘 不知不觉&#xff0c;已经加入CSDN这个大家庭5年多了&#xff0c;回想起3年前发布第一篇博客的时候&#xff0c;那时我记得很清楚&#xff0c;我在做项目时遇到报错&#xff0c;解决问题之后&#xff0c;然后想起了好多人…

Ipython和Jupyter Notebook介绍

Ipython和Jupyter Notebook介绍 Python、IPython和Jupyter Notebook是三个不同但密切相关的工具。简而言之&#xff0c;Python是编程语言本身&#xff0c;IPython是对Python的增强版本&#xff0c;而Jupyter Notebook是一种在Web上进行交互式计算的环境&#xff0c;使用IPytho…

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

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

时序分解 | Matlab实现CEEMDAN完全自适应噪声集合经验模态分解时间序列信号分解

时序分解 | Matlab实现CEEMDAN完全自适应噪声集合经验模态分解时间序列信号分解 目录 时序分解 | Matlab实现CEEMDAN完全自适应噪声集合经验模态分解时间序列信号分解效果一览基本介绍程序设计参考资料 效果一览 基本介绍 Matlab实现CEEMDAN完全自适应噪声集合经验模态分解时间…

C++(STL容器适配器)

前言&#xff1a; 适配器也称配接器&#xff08;adapters&#xff09;在STL组件的灵活组合运用功能上&#xff0c;扮演着轴承、转换器的角色。 《Design Patterns》对adapter的定义如下&#xff1a;将一个class的接口转换为另一个class的接口&#xff0c;使原本因接口不兼容而…

Eureka

大家好我是苏麟今天带来Eureka的使用 . 提供者和消费者 在服务调用关系中&#xff0c;会有两个不同的角色&#xff1a; 服务提供者&#xff1a;一次业务中&#xff0c;被其它微服务调用的服务。&#xff08;提供接口给其它微服务&#xff09; 服务消费者&#xff1a;一次业务…

CCF CSP认证 历年题目自练 Day22

CCF CSP认证 历年题目自练 Day22 题目一 试题编号&#xff1a; 201912-1 试题名称&#xff1a; 报数 时间限制&#xff1a; 1.0s 内存限制&#xff1a; 512.0MB 题目分析&#xff08;个人理解&#xff09; 每一个人都要报多少个数字&#xff0c;我选择字典存储&#xff0…

私有云盘:lamp部署nextcloud+高可用集群

目录 一、实验准备&#xff1a; 二、配置mariadb主从复制 三台主机下载mariadb 1&#xff09;主的操作 2&#xff09;从的操作 3&#xff09;测试数据是否同步 三、配置nfs让web服务挂载 1、安装 2、配置nfs服务器 3、配置web服务的httpd 4、测试 四、web 服务器 配…

进程调度算法之时间片轮转调度(RR),优先级调度以及多级反馈队列调度

1.时间片轮转调度算法(RR) round Robin 1.算法思想 公平地、轮流地为各个进程服务&#xff0c;让每个进程在一定时间间隔内都可以得到响应。 2.算法规则 按照各进程到达就绪队列的顺序&#xff0c;轮流让各个进程执行一个时间片&#xff08;如100ms&#xff09;。 若进程未…

C/C++学习 -- HMAC算法

1. HMAC算法概述 HMAC&#xff0c;全称为HMAC-MD5、HMAC-SHA1、HMAC-SHA256等&#xff0c;是一种在数据传输中验证完整性和认证来源的方法。它结合了哈希函数和密钥&#xff0c;通过在数据上应用哈希函数&#xff0c;生成一个带密钥的散列值&#xff0c;用于验证数据的完整性。…

10.1 今日任务:select实现服务器并发

#include <myhead.h>#define ERR_MSG(msg) do{\fprintf(stderr, "__%d__:", __LINE__); \perror(msg);\ }while(0)#define PORT 8888 //端口号&#xff0c;范围1024~49151 #define IP "192.168.112.115" //本机IP&#xff0c;ifco…

【Vue3】定义全局变量和全局函数

// main.ts import { createApp } from vue import App from ./App.vue const app createApp(App)// 解决 ts 报错 type Filter {format<T>(str: T): string } declare module vue {export interface ComponentCustomProperties {$filters: Filter,$myArgs: string} }a…

计算机竞赛 题目:基于FP-Growth的新闻挖掘算法系统的设计与实现

文章目录 0 前言1 项目背景2 算法架构3 FP-Growth算法原理3.1 FP树3.2 算法过程3.3 算法实现3.3.1 构建FP树 3.4 从FP树中挖掘频繁项集 4 系统设计展示5 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 基于FP-Growth的新闻挖掘算法系统的设计与实现…

httpserver 下载服务器demo 以及libevent版本的 httpserver

实现效果如下&#xff1a; 图片可以直接显示 cpp h 这些可以直接显示 其他的 则是提示是否要下载 单线程 还有bug 代码如下 先放上来 #include "httpserver.h" #include "stdio.h" #include <stdlib.h> #include <arpa/inet.h> #include…

QScrollArea样式

简介 QScrollBar垂直滚动条分为sub-line、add-line、add-page、sub-page、up-arrow、down-arrow和handle几个部分。 QScrollBar水平滚动条分为sub-line、add-line、add-page、sub-page、left-arrow、right-arrow和handle几个部分。 部件如下图所示&#xff1a; 样式详…

数据结构与算法(一):概述与复杂度分析

参考引用 Hello 算法 Github 仓库&#xff1a;hello-algo 1. 初识算法 1.1 算法无处不在 1.1.1 二分查找&#xff1a;查阅字典 在字典里&#xff0c;每个汉字都对应一个拼音&#xff0c;而字典是按照拼音字母顺序排列的。假设我们需要查找一个拼音首字母为 r 的字&#xff0…

美妆护肤品商城小程序的作用是什么?

化妆品几乎可以覆盖所有人群&#xff0c;各式各样的品牌及经销商非常多&#xff0c;主要销售模式为门店零售、线上入驻电商平台售卖、批发等&#xff0c;近些年随着电商发展迭代以及消费升级&#xff0c;对品牌或经销商来说&#xff0c;传统经营模式变得低效&#xff0c;每个人…

在word文档里面插入漂亮的伪代码

推荐用texsword.0.8 安装与界面 下载链接&#xff1a;https://sourceforge.net/projects/texsword/ 极为轻便&#xff0c;是Word的一个宏 安装过程也是极为简单&#xff0c;复制解压后的 texsword.dotm 文件到 C:\Users\{YOUR_USER_NAME}\AppData\Roaming\Microsoft\Word\ST…

全排列[中等]

优质博文&#xff1a;IT-BLOG-CN 一、题目 给定一个不含重复数字的数组nums&#xff0c;返回其所有可能的全排列。你可以按任意顺序返回答案。 示例 1&#xff1a; 输入&#xff1a;nums [1,2,3] 输出&#xff1a;[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]] 示例…

【MyBatis-Plus】快速精通Mybatis-plus框架—快速入门

大家在日常开发中应该能发现&#xff0c;单表的CRUD功能代码重复度很高&#xff0c;也没有什么难度。而这部分代码量往往比较大&#xff0c;开发起来比较费时。 因此&#xff0c;目前企业中都会使用一些组件来简化或省略单表的CRUD开发工作。目前在国内使用较多的一个组件就是…