C语言--动态内存管理

为什么存在动态内存管理?

在之前我们讲到的类型创建的变量再空间开辟好之后就不能再改变了,但是有时候我们希望能够自由地为某个变量分配空间,就比如上一篇文章中的通讯录,在没有动态内存管理情况下,我们就只能为通讯录开辟一点的空间,开辟之后就不能再更改了,如果一开始开辟的空间过大,可能会造成很大的浪费,如果一开始开辟的空间很小,那就可能不够用,当我们能够做到有几个联系人就开辟多大的空间时,空间利用率就会大大提升。这就是我们要讲的动态内存管理。

动态内存管理最重要的东西就是下面这四个函数:

malloc、calloc,realloc,free

malloc函数

malloc函数用于开辟内存快。他有一个参数,size,表示要开辟size个字节大小的空间,函数的返回值是这块空间的起始地址。

为什么返回的指针类型是void*呢? 因为malloc函数在设计时是不知道我们要开辟的这块空间的用途的,所以返回了一个void*的指针,然后我们要自己强制转换再使用。注意,malloc函数并不是每次申请空间都能成功,当申请空间失败时,函数会返回NULL,所以我们一定要对malloc函数的返回值进行有效性的判断,如果是空指针则说明开辟空间失败,这时候程序在执行下去已经没有意义了,我们可以打印错误信息在退出主程序, 对于参数size为0的情况,malloc的行为是标准未定义的,取决于编译器的实现。

int main()
{
	int* p = (int* ) malloc(40);
	if (p == NULL)
	{
		perror("malloc fail");
		return 1;
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i;
	}
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}

	return 0;
}

上面的代码中我们用malloc开辟了一块四十个字节的空间,并将其返回值强制转换为int*类型的指针。用一个指针变量接收返回值之后,对这块空间的使用就很简单了,我们在指针章节已经把指针的用法讲的很详细了。

在上面我们相当于用malloc开辟的空间实现了一个整型数组的功能,那么malloc申请的空间和正常创建的数组的空间有什么区别呢?

动态开辟的空间都是在堆区开辟的,想malloc、calloc和realloc申请的空间都是堆区的空间。像局部变量,形式参数这些临时性的变量都是在栈区开辟的。

free函数

我们用malloc等动态内存开辟函数申请的空间一般都会在程序退出时系统自动回收,我们也可以用free主动释放。如果我们的程序是那种一直在运行不退出的,如果动态申请的空间用完之后不及时用free函数释放的话,这一块空间就一直是有归属的,其他的程序无法使用它,这就会导致内存泄漏或者空间浪费。

free是用来释放动态开辟的空间的。

我们需要传给free函数一个动态开辟的空间的起始位置,这时free就会释放这块空间。

如果传给free的地址不是动态开辟的,那么free的行为是未定义的,取决于编译器的实现。

如果传给free一个空指针,那么free什么也不会做。

free(p);
p = NULL;

像上面的代码,我们用free释放的p所指向的动态开辟的空间,但是他并不会改变p的值,也就是说p还是指向那块空间的,这时的p可以说是一个野指针了,所以为了防止我们后面对p误操作导致非法访问,我们要及时对p置空。

错误的使用方法

free(p = NULL);

像这样的代码,p先被赋值成了空指针,这时free的参数是一个空指针,free什么也不会做,但是我们之前开辟的内存空间由于p的值被修改所以已经找不到了,这也导致了内存泄漏。

calloc

calloc的作用和malloc都是开辟一块内存空间,同时返回这块空间的首地址,只是传的参数有所不同,calloc两个参数。num是要开辟的元素个数,size是每个元素的大小,虽然传的参数有每个元素的大小,但是这块空间与malloc开辟的空间是一样的,返回的是一个无类型的void* 指针,所以还是需要我们对其强制转换后再使用,我们同样要对其有效性进行检查,因为他开辟空间失败的时候也会返回空指针。

cclloc与malloc开辟的空间有一个不同点,就是malloc开辟完空间之后就返回首地址了,而calloc再返回之前会对这块空间初始化

所以在使用这两个函数时,如果你想对这块空间初始化,就用calloc函数来开辟,如果你不想要初始化,就用malloc函数开辟空间。

int main()
{
	int* p1 = (int*)malloc(40);
	if (NULL == p1)
	{
		perror("mallocc fail");
		return 1;
	}
	int* p2 = (int*)calloc(10, 4);
	if (NULL == p2)
	{
		perror("calloc fail");
	}
	printf("%d\n", *p1);
	printf("%d\n", *p2);

	return 0;
}

realloc

前面的malloc和calloc函数虽然能够开辟出我们需要的空间大小,但是还没有达到调整大小的效果,而这里的realloc函数就是用来实现调整动态申请的空间的大小的,realloc函数的出现让动态内存管理更加灵活。

从这里我们可以看到realloc函数的参数有两个,一个是要调整的空间的而起始位置,另一个是调整后的空间大小。要注意的是,传过去的空间的起始地址必须是一个动态开辟的内存的指针,必须是malloc、calloc或者realloc函数的返回值的指针。

realloc函数有一个特点,就是当源地址空间后面的内存如果够要增加的大小,他就会在原地扩容,这时候返回的就是原来的地址。而如果后面的空间不够扩容,那他就会在堆区重新找一块足够大的空间来开辟,同时会把原空间的内容拷贝过来,然后自动释放掉原来的空间,并且返回新的起始地址。这一点不难理解,因为内存中空间是成块的,系统在分配内存时不可能让每一块空间之间都没有间隙,空间之间的间隙就是内存碎片,我们很难保证动态开辟的空间后面的空间都没有进程占用,所以realloc函数是很灵活的。

当然,realloc函数也可能扩容失败,这时候会返回空指针,为了保护原来的那一块空间,我们不要用原空间的指针去接受realloc的返回值,而要用一个新的指针去接收,再判断完指针的有效性之后再赋值给原指针。

int main()
{
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		perror("malloc fail");
		return 1;
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i;
	}
	int* ptr = (int*) realloc(p, 60);
	if (ptr == NULL)
	{
		perror("realloc fail");
		return 1;
	}
	p = ptr;
	ptr = NULL;
	for (i = 10; i < 15; i++)
	{
		*(p + i) = i;
	}
	for (i = 0; i < 15; i++)
	{
		printf("%d ", *(p + i));
	}
	free(p);
	p = NULL;
	return 0;
}

 

在这一次实现时,realloc是在原地扩容的。

realloc函数实现malloc函数的功能:

我们只需要给realloc函数的第一个参数传空指针,那么这个函数就与malloc函数的功能一样了。

int main()
{
	int* p = (int*)realloc(NULL, 40);
	if (p == NULL)
	{
		perror("realloc fail");
		return 1;
	}

	free(p);
	p = NULL;
	return 0;
}

讲到这里我们就能知道动态内存分配的基本使用了,主要就是要记住这三板斧:

函数返回值有效性检查,free释放空间,指针置为NULL

常见的动态内存错误:

1.对NULL指针的解引用,因为动态开辟内存可能会失败返回空指针,所以要检查指针是否为NULL

2.对动态开辟内存的越界访问,在使用开辟的动态内存时,我们一定要注意边界问题,不要越界。

3.对非动态开辟的内存使用free释放,如果这样做,程序会崩溃。

4.使用free释放一块动态开辟内存的一部分。再使用动态开辟内存时,我们要注意与原来的普通指针的使用相区别,在使用这些指针时,我们一定要保留动态内存的起始位置,传给free函数的也要是一块动态内存的起始位置,如果传的不是起始位置,程序会崩溃。

5.对同一块空间的多次释放程序也会崩溃。因为当我们第一次释放完p所指向的空间之后,这块空间就已经被操作系统回收了,不再属于我们的程序,但是p的值还没有变,还是指向那块空间,这时候p已经是个野指针了,我们不能对不属于自己的空间去进行释放。所以再写代码是,free完之后一点要及时置空。这样的话即使再次free,这时的p已经是空指针了,free什么也不会做i,但是我们还是要注意不要这样做。

6..动态开辟内存忘记释放,这就是我们前面说的内存泄漏问题了。

柔性数组

在C99标准中,结构体的最后一个元素允许是未知大小的的而数组,这就叫做柔性数组。

struct S
{
	int a;
	int arr[0];//元素个数为0表示还不确定数组的大小
	//如果写成0在有些编译器编不过去,那就把0删了写成下面这种形式
	//int arr[];
};

柔性数组的特点

1.结构体中的柔性数组成员前面必须有至少一个其他成员。

2.sizeof返回的结构体大小不包含柔性数组的内存。

3.包含柔性数组成员的结构用malloc等函数进行动态的内存分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。

我们可以这样分配内存

struct S* ps=(struct S*) malloc ( sizeof (struct S) + 要给柔性数组开辟的内存);

柔性数组使用时也要数组使用是一样的,ps->arr[ i ] 或者 *(ps->arr+i)

struct S
{
	int a;
	int arr[0];//元素个数为0表示还不确定数组的大小
	//如果写成0在有些编译器编不过去,那就把0删了写成下面这种形式
	//int arr[];
};

int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S) + 40);
    if ( ps==NULL)
    {
    perror("malloc fail");
    return 1;
    }
	int i = 0;
	for(i=0;i<10;i++)
	{
		ps->arr[i] = i;
	}
	for (i = 0; i < 10; i++)
	{
		printf("%d ", ps->arr[i]);
	}

	free(ps);
	ps = NULL;

	return 0;
}

柔性数组的柔性体现在哪里?

当我们发现一开始为数组预留的空间不够我们使用时,这时候我们就可以用realloc函数对这块进行扩容。

可能有人会说,柔性数组好像并不是很有必要存在,因为我们似乎可以单纯用一个指针来指向一块动态开辟的内存来实现这种功能,而指针指向的这块空间也是可以调整的。

struct S
{
	int a;
	int* p;
};

int main()
{
	struct S * ps = (struct S*)malloc(sizeof(struct S));
	if (ps == NULL)
	{
		perror("malloc fail");
		return 1;
	}
	ps->p = (int*)malloc(40);
	if (ps->p == NULL)
	{
		perror("malloc fail");
		return 1;
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		ps->p[i] = i;
	}
	int* ptr=(int*)realloc(ps->p,60);
	if (ptr == NULL)
	{
		perror("realloc fail");
		return 1;
	}
	ps->p = ptr;
	ptr = NULL;
	for (i = 10; i < 15; i++)
	{
		ps->p[i] = i;
	}
	for (i = 0; i < 15; i++)
	{
		printf("%d ", ps->p[i]);
	}
	free(ps->p);
	free(ps);
	ps = NULL;

	return 0;
}

在上面这段代码中,我们为了和柔性数组的实现对标,结构体也是用nalloc动态分配内存,从这里我们就能发现一些柔性数组的优点,或者说一些指针实现的缺点:当我们用这种指针实现数组时,给结构体要malloc,创建数组也要malloc,这种方案开辟的空间是分开的,而且当malloc使用的次数多了之后,可能导致内存之间留下碎片,空间利用率不高,而结构体柔性数组的空间和数组是连续的,不会导致内存碎片。同时,在使用这种指针的时候,我们要进行两次free,要注意free的顺序问题。

柔性数组的优势:

1.方便内存释放

2.提高访问速度(提高得也有限),有益于减少内存能碎片

在学习完动态内存开辟之后,我们就能够实现动态版本的通讯录了,下一篇文章会对之前的通讯录进行一些修改,节省内存的同时不用担心空间不够。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

18.字面量

文章目录 一、字面量二、区分技巧三、扩展&#xff1a; /t 制表符 一、字面量 在有些资料&#xff0c;会把字面量说成常量、字面值常量&#xff0c;这种叫法都不是很正确&#xff0c;最正确的叫法还是叫做&#xff1a;字面量。 作用&#xff1a;告诉程序员&#xff0c;数据在…

地物波谱库共享网站汇总

ENVI自5.2版本重新梳理了原有的标准波谱库&#xff0c;新增一些物质波谱&#xff0c;在ENVI5.6中存放在…\Harris\ENVI56\ resource\speclib&#xff0c;分别存放在四个文件夹中&#xff0c;储存为ENVI波谱库格式&#xff0c;有两个文件组成&#xff1a;.sli和.hdr。 ENVI保留…

小米还涉足了哪些领域

小米作为一家全球性的移动互联网企业&#xff0c;其业务领域相当广泛&#xff0c;除了核心的智能手机业务外&#xff0c;还涉足了许多其他领域。以下是对小米涉足领域的简要介绍&#xff1a; 智能硬件与IoT平台&#xff1a;小米是全球领先的智能硬件和IoT平台公司&#xff0c;致…

linux:线程同步

个人主页 &#xff1a; 个人主页 个人专栏 &#xff1a; 《数据结构》 《C语言》《C》《Linux》 文章目录 前言线程同步条件变量接口简单示例pthread_cond_wait为什么要有mutex伪唤醒问题的解决 (if->while) 总结 前言 本文作为我对于线程同步知识总结 线程同步 同步&…

资讯头条P3自媒体搭建

自媒体素材管理与文章管理 一.后台搭建 1.1 搭建自媒体网关 导入网关模块>>>在网关模块的pom.xml文件中添加该子模块>>>刷新maven <modules><module>heima-leadnews-app-gateway</module><!--新增--><module>heima-leadnew…

Aurora插件安装

介绍 Latext是一种基于TEX的排版系统。 CTeX中文套装是基于Windows下的MiKTeX系统&#xff0c;集成了编辑器WinEdt和PostScrip处理软件Ghostscript和GSview等主要工具。CTeX中文套装在MikTeX的基础上增加了对中文的完整支持。 CTeX&#xff1a; CTeX套装 - CTEX 下载安装 然后…

【Java程序设计】【C00345】基于Springboot的船舶监造管理系统(有论文)

基于Springboot的船舶监造管理系统&#xff08;有论文&#xff09; 项目简介项目获取开发环境项目技术运行截图 项目简介 项目获取 &#x1f345;文末点击卡片获取源码&#x1f345; 开发环境 运行环境&#xff1a;推荐jdk1.8&#xff1b; 开发工具&#xff1a;eclipse以及i…

Day50:WEB攻防-PHP应用文件包含LFIRFI伪协议编码算法无文件利用黑白盒

目录 文件包含-原理&分类&利用&修复 文件读取 文件写入 代码执行 远程利用思路 黑盒利用-VULWEB 白盒利用-CTFSHOW-伪协议玩法 78-php&http协议 79-data&http协议 80-81-日志包含 87-php://filter/write&加密编码 88-data&base64协议 …

韩顺平Java | C21网络编程

1 网络的相关概念 ip地址的组成&#xff1a;网络地址 主机地址 A类&#xff1a;0 ~ 2^7-1 0 ~ 127 B类&#xff1a;128 ~ 1282^6-1 128 ~ 191 C类&#xff1a;192 ~ 1922^5-1 192 ~ 223 D类&#xff1a;224 ~ 2242^4-1 224 ~ 239 E类&#xff1a;240 ~ 2402^3-1 240 ~ 2…

Unity PS5开发 天坑篇 之 URP管线与HDRP管线部署流程以及出包介绍04

目录 一, URP管线、HDRP管线下的Unity项目部署 1. PS5开发论坛关于Unity可支持的版本说明: 2. URP管线下的项目与部署 2.1 Build PS5 URP Project 2.2 运行画面 3. HDRP管线下的项目与部署 3.1 附上可以运行的画面: 4. PS5打包方式介绍 4.1 PC串流调试模式: Build Typ…

selenium自动化测试

selenium自动化测试 1、Javaselenium环境搭建2、测试&#xff0c;打开任意网页3、selenium 常见的Api3.1元素定位findElement3.1.1 css 选择语法3.1.2 xpath 选择语法 1、Javaselenium环境搭建 下载chromedriver&#xff0c;版本要与Chrome浏览器版本一致。 下载之后将chro…

算法打卡day25|回溯法篇05|Leetcode 491.递增子序列、46.全排列、47.全排列 II

算法题 Leetcode 491.递增子序列 题目链接:491.递增子序列 大佬视频讲解&#xff1a;递增子序列视频讲解 个人思路 和昨天的子集2有点像&#xff0c;但昨天的题是通过排序&#xff0c;再加一个标记数组来达到去重的目的。 而本题求自增子序列&#xff0c;是不能对原数组进行…

springboot检测脚本

import requests import urllib3 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) session requests.session()# 从文本文件中读取 with open(dic.txt, r) as file:paths file.readlines()# 移除每个末尾的换行符 paths [path.strip() for path in pa…

线程创建方式、构造方法和线程属性

欢迎各位&#xff01;&#xff01;&#xff01;推荐PC端观看 文章重点&#xff1a;学会五种线程的创造方式 目录 1.开启线程的五种方式 2.线程的构造方法 3.线程的属性及获取方法 1.开启线程的五种方式 创造线程的基本两步&#xff1a;&#xff08;1&#xff09;使用run方法…

并发编程之Callable方法的详细解析(带小案例)

Callable &#xff08;第三种线程实现方式&#xff09; Callable与Runnable的区别 Callable与Runnable的区别 实现方法名称不一样 有返回值 抛出了异常 ​class Thread1 implements Runnable{Overridepublic void run() { ​} } ​ class Thread2 implements Callable<…

x86的内存分段机制

8086 是 Intel 公司第一款 16 位处理器&#xff0c;诞生于 1978 年&#xff0c;所以说它很古老。 一.8086 的通用寄存器 8086 处理器内部共有 8 个 16 位的通用处理器&#xff0c;分别被命名为 AX、 BX、 CX、 DX、 SI、 DI、 BP、 SP。如下图所示。 “通用”的意思是…

【JavaSE】String类详解

目录 前言 1. 什么是String类 1.1 String的构造 1.2 String类的基本操作&#xff1a;打印、拼接、求字符串长度 2. String类的常用方法 2.1 字符串查找 2.2 字符串替换 2.3 字符串拆分 2.4 字符串截取 2.5 字符串和其他类型的转换 2.6 去除字符串左右两边的空格 3.…

日赚2000万的短剧,还能火多久?

沈瑶初十年前就义无反顾地爱上高禹川&#xff0c;当他们两人再次相遇&#xff0c;她主动靠近高禹川&#xff0c;不料&#xff0c;她却意外怀孕&#xff0c;高禹川为了负责选择领证&#xff0c;但不公布两人的关系...... 这是一部情绪稳定女航医与傲娇疯批男机长的虐恋剧。在这个…

【MongoDB】一问带你深入理解什么是MongDB,MongoDB超超详细保姆级教程

目录 1、MongoDB概述2、MongoDB 主要特点2.1、文档2.2、集合2.3、数据库2.4、数据模型 3、Windows安装MongoDB3.1、下载MongoDB3.2、安装MongoDB3.3、配置MongoDB 4、Linux安装MongoDB4.1、下载MongoDB4.2、解压安装4.3、安装一个可视化工具 5、MongoDB基本操作及增删改查5.1、…

【案例·增】获取当前时间、日期(含,SQL中DATE数据类型)

问题描述&#xff1a; 需要使用当前时间、日期&#xff0c;可以使用 SQL 中的 CURDATE() 、NOW()、CURTIME()运算符 案例&#xff1a; INSERT INTO table_name(current_time, column_name2,...) VALUES (NOW(),, ...)规则(Date 相关函数)&#xff1a; 规则(Date数据类型)