【C语言】动态内存管理(C语言的难点与精华,数据结构的前置知识,你真的掌握了吗?)

news2024/9/24 5:30:18

文章目录

  • 引言
  • 一、为什么要动态内存分配
  • 二、动态内存分配的相关函数
    • 2.1 malloc
    • 2.2 free
    • 2.3 calloc
    • 2.4 realloc
  • 三、常见的动态内存的错误
    • 3.1 对NULL指针的解引用
    • 3.2 对动态内存越界访问
    • 3.3 对非动态内存释放
    • 3.4 对动态内存部分释放
    • 3.5 对动态内存多次释放
    • 3.6 未对动态内存释放(内存泄漏)
  • 四、动态内存经典笔试题分析
    • 4.1 题目一
    • 4.2 题目二
    • 4.3 题目三
    • 4.4 题目四
  • 五、柔性数组
    • 5.1 柔性数组的特点
    • 5.2 柔性数组的使用
  • 六、C/C++中程序内存区域划分

引言

学习专栏

《零基础学C语言》
《数据结构世界》

俗话说的好,要想学好数据结构(数据结构世界,对数据结构感兴趣的小伙伴可以移步),就必须学好以下三方面知识:

  1. 指针
    不允许你还不了解指针的那些事(一)(内存和地址+指针变量+指针运算+野指针+传址调用)
    不允许你还不了解指针的那些事(二)(数组传参的本质+冒泡排序+数组指针+指针数组)
  2. 结构体
    自定义类型:结构体(你真的掌握了内存对齐,位段吗?)
  3. 动态内存管理

前两方面的知识在往期已经详细讲解,今天我们就来学习最后一方面的知识——动态内存管理

一、为什么要动态内存分配

我们已经掌握的内存开辟方式有:

int val = 20;//在栈空间上开辟四个字节
char arr[10] = {0};//在栈空间上开辟10个字节的连续空间

但是上述的开辟空间的方式有两个特点:

  • 空间开辟大小是固定的。
  • 数组在申明的时候,必须指定数组的长度,数组空间⼀旦确定了大小不能调整

所以,为了能在程序运行中,根据需求灵活地调整空间大小,C语言引入了动态内存管理。

二、动态内存分配的相关函数

2.1 malloc

C语言提供了一个动态内存开辟的函数:

void* malloc (size_t size);

这个函数可以开辟一块连续的内存空间,并返回指向这块空间的指针

  • 如果开辟成功,则返回指向开辟好空间的指针。
  • 如果开辟失败,则返回NULL 指针,因此malloc的返回值一定要做检查。
  • 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
int main()
{
	//动态开辟10个整型空间
	int* a = (int*)malloc(10 * sizeof(int));
	if (a == NULL)//开辟不成功,返回NULL
	{
		perror("malloc fail");
		return 1;
	}
	//开辟成功,使用该空间
	//...
	return 0;
}

注意:malloc(0)——开辟0个字节空间,是标准未定义行为,具体取决于编译器

2.2 free

C语言提供了另外一个函数free,可以动态内存的释放和回收:

void free (void* ptr);

free函数用来释放动态开辟的内存

  • 如果参数 ptr 是指向动态内存空间的指针,则释放该空间
  • 如果参数 ptr 是NULL指针,则不作任何处理。
int main()
{
	//动态开辟10个整型空间
	int* a = (int*)malloc(10 * sizeof(int));
	if (a == NULL)//开辟不成功,返回NULL
	{
		perror("malloc fail");
		return 1;
	}
	//开辟成功,使用该空间
	//使用完毕,释放该空间
	free(a);
	a = NULL;
	return 0;
}

注意:如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的

2.3 calloc

C语言还提供了一个函数叫 calloc , calloc 函数也用来动态内存分配。

void* calloc (size_t num, size_t size);

其实,它与malloc很相似,区别在于calloc会将开辟的连续空间,每个字节都初始化为0

int main()
{
	//动态开辟10个整型空间并初始化为0
	int* a = (int*)calloc(10, sizeof(int));
	if (a == NULL)//开辟不成功,返回NULL
	{
		perror("calloc fail");
		return 1;
	}
	//开辟成功,使用该空间
	//使用完毕,释放该空间
	free(a);
	a = NULL;
	return 0;
}

2.4 realloc

realloc函数的出现让动态内存管理更加灵活。
为了合理的运用内存,所以在开辟空间后觉得太大或太小,就可以使用realloc进行调整。

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

realloc调整成功,会返回调整后指向这块空间的指针

  • ptr 是调整前空间的指针
  • size 是调整后的新大小
  • 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。

realloc在调整内存空间的是存在两种情况:

  • 情况1:原有空间之后有足够大的空间

这个时候,realloc会直接在原有空间后面直接追加开辟。

  • 情况2:原有空间之后没有足够大的空间

这个时候,realloc就会再找一块足够大空间,一次性开辟完,再将原数据拷贝过去。

int main()
{
	//动态开辟10个整型空间
	int* a = (int*)malloc(10 * sizeof(int));
	if (a == NULL)//开辟不成功,返回NULL
	{
		perror("malloc fail");
		return 1;
	}
	//调整空间,扩大为20个整型空间
	int* tmp = (int*)realloc(a, 20 * sizeof(int));
	if (tmp == NULL)//开辟不成功,返回NULL
	{
		perror("realloc fail");
		return 1;
	}
	//调整成功,将空间地址给a
	a = tmp;
	//使用空间...
	free(a);
	a = NULL;
	return 0;
}

注意

  • 用临时变量tmp来接受realloc调整后的空间地址
  • 因为如果用a接收,万一调整失败返回NULL,还会丢掉原本空间的地址

三、常见的动态内存的错误

3.1 对NULL指针的解引用

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

3.2 对动态内存越界访问

void test()
{
	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.3 对非动态内存释放

void test()
{
	int a = 10;
	int *p = &a;
	free(p);//ok?
}

3.4 对动态内存部分释放

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

3.5 对动态内存多次释放

void test()
{
	int *p = (int *)malloc(100);
	free(p);
	free(p);//重复释放
}

3.6 未对动态内存释放(内存泄漏)

因为程序终止会回收所有空间,所以这里用死循环来模拟程序运行中不释放动态内存,会导致内存泄漏。

int main()
{
	int* a = (int*)malloc(10 * sizeof(int));
	while(1);
	return 0;
}

总结动态开辟的内存一定要释放,并且要正确释放

四、动态内存经典笔试题分析

请先自行思考哦,不要立马看解析~

请问以下题目运行Test 函数会有什么样的结果?

4.1 题目一

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

结果:

  • 程序崩溃,内存泄漏

解析:

  • GetMemory函数中,动态开辟了100个字节空间。但是p是一个形参,形参是实参的一份临时拷贝,对形参的影响无法改变实参,所以str还是NULL。
  • 在运行strcpy函数时,因为要拷贝的目的地是NULL,所以程序崩溃。
  • 同时,动态开辟的空间未释放,还导致了内存泄漏。

4.2 题目二

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

结果:

  • 有可能打印出乱码

解析:

  • GetMemory函数中,开辟了p数组。p是局部变量,出了函数作用域就会销毁(空间被回收)。
  • 所以,str接收p的地址时,此时已经指向一块被回收的空间。
  • 打印该空间的内容,是否为乱码,取决于该空间的内容是否被其余数据覆盖

4.3 题目三

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

结果:

  • 内存泄漏,有可能程序崩溃

解析:

  • 这次进行传址调用,二级指针解引用,确实能改变外部str
  • 但是接收到空间地址后,未进行检查,如果开辟失败,为NULL,则strcpy时依旧会程序崩溃
  • 同时未对动态内存释放,造成内存泄漏

4.4 题目四

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

结果:

  • 后果未知

解析:

  • 假设能正常开辟动态内存
  • strcpy拷贝完hello,并free进行释放
  • 因为free不会自动将str置为NULL,所以以下判断不起作用,“拦不住”str这个野指针
  • 最后strcpy将world拷贝到已被释放的空间,再进行打印(对已被释放的动态内存进行操作,后果未知,十分危险,很严重!)

五、柔性数组

也许你从来没有听说过柔性数组(flexible array)这个概念,但是它确实是存在的。
C99 中,结构中的最后⼀个元素允许是未知大小的数组,这就叫做『柔性数组』成员。

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

有些编译器会报错无法编译可以改成:

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

5.1 柔性数组的特点

  • 结构中的柔性数组成员前面必须至少一个其他成员
  • sizeof 返回的这种结构大小不包括柔性数组的内存
  • 包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
typedef struct st_type
{
	int i;
	int a[0];//柔性数组成员
}type_a;
int main()
{
	printf("%d\n", sizeof(type_a));//输出的是4
	return 0;
}

5.2 柔性数组的使用

int main()
{
	int i = 0;
	type_a *p = (type_a*)malloc(sizeof(type_a)+100*sizeof(int));
	//使用空间
	p->i = 100;
	for(i=0; i<100; i++)
	{
		p->a[i] = i;
	}
	free(p);
	return 0;
}

这样柔性数组成员a,相当于获得了100个整型元素的连续空间。

六、C/C++中程序内存区域划分

C/C++程序内存分配的几个区域:

  1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
  2. 堆区(heap):一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。分配方式类似于链表。
  3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
  4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。

看到这里了还不给博主扣个:
⛳️ 点赞☀️收藏 ⭐️ 关注!
💛 💙 💜 ❤️ 💚💓 💗 💕 💞 💘 💖
拜托拜托这个真的很重要!
你们的点赞就是博主更新最大的动力!
有问题可以评论或者私信呢秒回哦。

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

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

相关文章

DevEco Studio自定义代码颜色

这里以ArkTS代码颜色举例 进入设置&#xff08;快捷键CtrlAltS&#xff09; 选择Editor > Color Scheme > JavaScript 由于之前用习惯VsCode了&#xff0c;这里以注释颜色举例&#xff0c;变为绿色。 上面说的不是以ArkTS代码颜色举例吗&#xff1f;为什么选择JavaScr…

使用下载代替物理串口输出-STM32 Debug (printf) Viewer

使用下载代替物理串口输出-STM32 Debug 硬件要求配置方法代码要求打印输出结果 硬件要求 STM32的PB9、PB10引脚的串口1通常用作其他功能使用后&#xff0c;无法通过printf()函数打印输出想要调试输出查看变量或调试信息。现已使用另外一种方法实现printf()函数打印输出。 ST…

BugKu-Web-滑稽

题目环境 持续的动态图片 F12审查元素 拿下flag&#xff1a;flag{595d994a34342417bfc3a3c3a23e0a48}

Java: Random

/*** encoding: utf-8* 版权所有 2023 涂聚文有限公司* 许可信息查看&#xff1a;* 描述&#xff1a; //https://commons.apache.org/proper/commons-lang/javadocs/api-2.6/org/apache/commons/lang/RandomStringUtils.html* //https://commons.apache.org/pro…

jmeter 如何循环使用接口返回的多值?

有同学在用jmeter做接口测试的时候&#xff0c;经常会遇到这样一种情况&#xff1a; 就是一个接口请求返回了多个值&#xff0c;然后下一个接口想循环使用前一个接口的返回值。 这种要怎么做呢&#xff1f; 有一定基础的人&#xff0c;可能第一反应就是先提取前一个接口返回…

测试用例设计方法之判定表详解!!

理论部分 判定表是分析和表达多种输入条件下系统执行不同动作的工具&#xff0c;它可以把复杂的逻辑关系和多种 条件组合的情况表达得既具体又明确。 条件桩(Condition Stub)动作桩(Action Stub&#xff09;条件项(Condition Entry&#xff09;动作项(Action Entry&#xff0…

Redis常用内存淘汰策略?

从淘汰范围来说可以分为不淘汰任何数据、只从设置了到期时间的键中淘汰和从所有键中淘汰三类。而从淘汰算法来分&#xff0c;又主要分为 random&#xff08;随机&#xff09;&#xff0c;LRU&#xff08;最近最少使用&#xff09;&#xff0c;以及 LFU&#xff08;最近最不常使…

【AI美图】第03期效果图,AI人工智能全自动绘画,二次元美图欣赏

带来一组二次元人工智能自动绘图 对比分析&#xff1a; 标题手画二次元需要技巧&#xff1a; 二次元高清图片的绘制技巧主要包括以下几点&#xff1a; 线条的运用&#xff1a;在二次元风格的绘画中&#xff0c;线条的运用非常重要。要绘制出流畅、细腻的线条&#xff0c;需…

VBA技术资料MF96:单字段多条件高级筛选

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

基于CNN+数据增强+残差网络Resnet50的少样本高准确度猫咪种类识别—深度学习算法应用(含全部工程源码)+数据集+模型(六)

系列文章目录 基于CNN数据增强残差网络Resnet50的少样本高准确度猫咪种类识别—深度学习算法应用(含全部工程源码)数据集模型&#xff08;一&#xff09; 基于CNN数据增强残差网络Resnet50的少样本高准确度猫咪种类识别—深度学习算法应用(含全部工程源码)数据集模型&#xf…

Day10 Liunx高级系统设计11-数据库2

DQL:数据查询语言 查询全表 select * from 表名; 查询指定列 select 列名 1, 列名 2,… from 表名 ; 条件查询 select * from 表名 where 条件 ; 注意&#xff1a; 条件查询就是在查询时给出 WHERE 子句&#xff0c;在 WHERE 子句中可以使用如下运算符及关键 字&#…

学习笔记10——Mysql的DDL语句

学习笔记系列开头惯例发布一些寻亲消息 链接&#xff1a;https://baobeihuijia.com/bbhj/contents/3/197161.html 数据库创建&#xff1a; CREATE DATABASE books&#xff1b; CREATE DATABASE IF NOT EXISTS books;更改字符集 ALTER DATABASE books CHARACTER SET gbk;库的删…

三层交换与DHCP

目录 一、三层交换 &#xff08;一&#xff09;基本概念 &#xff08;二&#xff09;转发原理 &#xff08;三&#xff09;ensp项目实验 二、DHCP &#xff08;一&#xff09;DHCP工作原理 1.DHCP的特点 2.工作原理 &#xff08;二&#xff09;DHCP项目实验 一、三层交…

电商用户运营优化:腾讯文档API无代码集成

腾讯文档API集成&#xff1a;电商平台无代码客服系统 随着数字化时代的到来&#xff0c;电商平台日益求势于高效率和低成本的运营模式。无代码开发正在改变传统业务流程的构建方式&#xff0c;尤其在客户服务领域&#xff0c;这种转变正变得尤为明显。本文将探索腾讯文档API在…

大模型算法工程师的面试题来了(附答案)

自 ChatGPT 在去年 11 月底横空出世&#xff0c;大模型的风刮了整一年。 历经了百模大战、Llama 2 开源、GPTs 发布等一系列里程碑事件&#xff0c;将大模型技术推至无可争议的 C 位。基于大模型的研究与讨论&#xff0c;也让我们愈发接近这波技术浪潮的核心。 最近大模型相关…

【QT】时间日期与定时器

目录 1.时间日期相关的类 2.日期时间数据与字符串之间的转换 2.1 时间、日期编辑器属性设置 2.2 日期时间数据的获取与转换为字符串 2.3 字符串转换为日期时间 3.QCaIendarWidget日历组件 3.1基本属性 3.2 公共函数 3.3 信号 4.实例程序演示时间日期与定时器的使用 …

创建型模式之工厂方法模式

一、概述 1、工厂方法模式&#xff1a;定义一个用于创建对象的接口&#xff0c;让子类决定将哪一个类实例化。工厂方法模式让一个类的实例化延迟到其子类 2、工厂方法模式&#xff1a;不再提供一个按钮工厂类来统一负责所有产品的创建&#xff0c;而是将具体的按钮创建过程交…

一文弄懂自编码器 -- Autoencoders

1. 引言 近年来&#xff0c;自编码器&#xff08;Autoencoder&#xff09;一词在许多人工智能相关的研究论文、期刊和学位论文中被频繁提及。自动编码器于1980年推出&#xff0c;是一种用于神经网络的无监督学习技术&#xff0c;可以从未被标注的训练集中学习。 本文重点介绍…

谣言检测常用数据集汇总

Pheme-R 获取地址&#xff1a;https://figshare.com/articles/dataset/PHEME_rumour_scheme_dataset_journalism_use_case/2068650 PHEME社交媒体谣言数据集:这些谣言与9条不同的突发新闻有关。它是为分析社交媒体谣言而创建的&#xff0c;并包含由谣言推文发起的推特对话;这些…

设计模式——装饰模式(结构型)

引言 装饰模式是一种结构型设计模式&#xff0c; 允许你通过将对象放入包含行为的特殊封装对象中来为原对象绑定新的行为。 假设你正在开发一个提供通知功能的库&#xff0c; 其他程序可使用它向用户发送关于重要事件的通知。 库的最初版本基于 通知器Notifier类&#xff0c;…