【C语言必学知识点七】坚决不允许你还不知道C/C++程序如何对内存进行分区!!!

news2024/11/14 14:09:28

动态内存管理——详细解读C/C++程序中的内存分区

  • 导读
  • 一、C/C++程序中的内存分区
  • 二、常见的动态内存的错误
    • 2.1 内存开辟失败后对空指针进行解引用
    • 2.2 对已开辟好的空间进行越界访问
    • 2.3 free不是有动态函数开辟的空间
    • 2.4 free动态内存开辟空间的一部分
      • 2.4.1 free函数的底层逻辑
    • 2.5 未对开辟的空间进行释放导致内存泄漏
    • 2.6 对同一块空间进行多次释放
  • 结语

封面

导读

大家好,很高兴又和大家见面啦!!!

在前面的内容中,我们已经介绍完了4个动态函数及其使用。不知道大家在前面的内容中有没有过一种疑惑——为什么同样是申请空间,通过动态函数申请的空间可以进行大小的修改,而创建变量或数组时申请的空间确无法进行空间大小的修改?

相信有细心的小伙伴在前面的函数介绍中有发现这么一句话:

  • 在调试过程中,如何管理更多的堆的信息,请参阅C运行库调试支持。

这里提到的堆究竟是什么呢?难道这个堆跟动态内存管理是有什么联系吗?下面我们就一起来探讨一下;

一、C/C++程序中的内存分区

在计算机的世界里,所有的事物都是由数据构成,并且这些数据在计算机中都是以同一种形式存在——电信号。

现代的计算机结构都是冯·诺依曼结构——计算机由控制器、存储器、运算器、输入设备、输出设备这五大部件构成。

在冯·诺依曼机器中,所有的数据与指令都是以二进制的形式存储在存储器中,计算机能够识别不同的指令与数据。

为了更好的利用存储器的内存空间,C/C++程序将内存空间分成了以下几个部分:

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

内存分区

下面我以一段代码来理解这些不同的分区:

//定义函数
void func1() {

}
//定义全局变量
int global_value = 1;
//C/C++中的内存分区
void test() {
	//定义静态变量
	static int static_value = 2;
	//定义局部变量
	int local_value = 3;
	//定义数组
	char str[4] = "abc";
	//调用函数
	func1();
	//申请动态内存
	int* p = (int*)calloc(3, sizeof(int));
	if (!p) {
		perror("calloc");
		return;
	}
	free(p);
}

在这段代码中,我们分别创建了全局变量、静态变量、局部变量、字符数组,调用了函数,通过动态函数申请了空间以及释放了申请的空间。

那么对于这段代码中的各个元素,他们在内存中又分别位于什么位置呢?下面我们就一起来看一下:

内存分区2
从上图中我们可以根据这些变量和值的字体颜色来进行它们对应区域的划分:

  • 红色字体:栈区
  • 蓝色字体:堆区
  • 绿色字体:数据段
  • 橙色字体:代码段

现在我们就明白了,我们在进行动态内存申请时,内存空间申请是在堆区完成,而空间地址也就是函数的返回值则是由栈区的指针进行接收。

也就是说动态函数管理的内存空间实际上是管理的堆区,而创建的变量、数组是在栈区。

这也很好的解释了为什么同样是申请内存,但是动态函数申请的内存可以主动的被释放,而变量、数组所申请的内存空间不能主动释放。就是因为他们所分配的内存空间不相同。

现在动态内存管理的内容我们就介绍完了,接下来我们就来继续探讨一下如何正确的使用动态内存;

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

在动态内存管理中,由于整个过程分别涉及到栈区的指针变量以及堆区的内存空间,因此我们如果使用不当的话,很容易会照成一些错误。在动态内存管理中,大致有以下几点常见错误:

2.1 内存开辟失败后对空指针进行解引用

malloccallocrealloc这三个函数在申请内存空间时都会存在两种情况:

  • 内存开辟成功:返回指向内存起始地址的指针
  • 内存开辟失败:返回空指针

因此,如果我们在进行内存申请后,未对返回值及时的进行判空操作,那么就很容易在后续操作中出现对空指针解引用的问题。如下所示:

//常见错误1——内存开辟失败后对空指针进行解引用
#include <limits.h>
void test7() {
	//通过动态函数申请空间
	int* p = (int*)calloc(INT_MAX, sizeof(int));
	for (int i = 0; i < 5; i++) {
		p[i] = i + 1;
	}
	free(p);
}

这里我们可以看到,此时我通过calloc申请内存空间后,并未对指针p进行判空,而是直接对指针p进行解引用,这种情况下就很容易出现对空指针解引用的错误。

因此为了避免这种错误的产生,我们一定要注意:

  • 内存空间申请完后,对返回值进行判空操作

2.2 对已开辟好的空间进行越界访问

当我们通过内存函数开辟空间时,实际上就是在堆区申请了一块连续的内存空间。

当我们的空间申请好后,在进行访问时,该空间的大小是无法被改变的。比如我申请了10个字节的空间,那么我也就只能够访问十个字节的空间。当我想要访问第11个字节的空间时,此时就会发生越界访问的问题。如下所示:

//常见错误2——对已开辟好的空间进行越界访问
void test8() {
	//通过动态函数申请10个整型空间
	int* p = (int*)calloc(10, sizeof(int));
	//完成申请后对p及时进行判空操作,防止出现错误1
	if (!p) {
		perror("calloc");
		return;
	}
	for (int i = 0; i < 20; i++) {
		p[i] = i + 1;
	}
	free(p);
}

在这个例子中,我们通过calloc只申请了10个整型空间,但是在进行访问时,我们设置的边界却是20个整型空间,因此当代码完成第10个整型空间后继续访问第11个空间时,就会发生越界访问。

这时可能就有朋友要反驳了——你不是说这个堆区的空间是可以实时进行调整的吗?怎么现在又不能调整了呢?

这是因为动态内存管理的动态体现在我们可以通过动态函数来改变内存空间的大小,而不是我们在访问内存空间时,它的大小能够随意的被改变,这一点一定要注意!!!

因此为了避免出现越界访问的问题,我们一定要确定好访问的边界,如申请了10个字节的空间,那我们就只能够访问从起始地址开始的10个字节以内的空间。

2.3 free不是有动态函数开辟的空间

这个问题在前面我们也提到过,free函数能够主动释放的只有堆区的空间,而堆区的空间只能够通过malloccalloc以及realloc这些动态函数来进行申请,当我们通过free来释放栈区的空间时,那就会发生错误。如下所示:

//常见错误3——free不是有动态函数开辟的空间
void test9() {
	//创建大小为10的整型数组
	int arr[10] = { 0 };
	//通过动态函数创建10个整型空间
	int* Arr = (int*)calloc(10, sizeof(int));
	if (!Arr) {
		perror("calloc");
		return;
	}
	//通过free释放数组空间
	free(arr);
}

在这个例子中,我们分别在堆区和栈区创建了10个整型空间。两个指针名虽然都是arr,但是首字母大写的指针名执行的空间是堆区的空间,小写的指针名指向的空间是栈区的空间。最后我们在释放空间时,通过free释放的是小写的arr,也就是释放的栈区的空间。

显然这种操作是错误的,在内存空间中,栈区的空间只能够有操作系统进行回收,只有堆区的空间才能够由程序员主动释放,因此free函数能够释放的是指向堆区的指针Arr,这个指针名是大写开头。

为了避免这个问题的出现,我们就需要特别注意指针名以及空间申请的方式。传入到函数的指针一定得是指向由动态函数开辟的空间。

2.4 free动态内存开辟空间的一部分

这个问题的出现一般是在进行空间释放时,我们给函数传入的不是空间的起始地址,如下所示:

//常见错误4——free动态内存开辟空间的一部分
void test10() {
	//创建大小为10的整型数组
	int P[10] = { 0 };
	//创建10个整型空间
	int* p = (int*)calloc(10, sizeof(int));
	//及时进行判空,避免错误1
	if (!p) {
		perror("calloc");
		return;
	}
	//控制好访问边界,避免错误2
	for (int i = 0; i < 5; i++) {
		*p = i + 1;
		p++;
	}
	//区分好指针指向的空间,避免错误3
	free(p);
}

在这个例子中我们可以看到,从申请空间,到访问空间,最后到释放空间,前三个问题我们都已经避免了,现在感觉代码没啥问题对吧。

但是这个代码是存在问题的,注意看我们在对空间进行访问时,我们的访问方式是怎样的?

没错,我们的访问方式不是通过下标来逐一访问各个空间,而是通过移动指针并进行解引用完成的访问。在我们没有记录起始地址的情况下,这种访问方式就会让我们丢失已经被访问过的空间,最后传入函数的地址并不是空间的起始地址。这样free函数会判定该空间并不是有效的空间。

导致这个错误的原因是我们对free函数的底层逻辑不太理解。那么free函数的底层逻辑是什么呢?下面我就来说一下我对这个逻辑的简单理解;

2.4.1 free函数的底层逻辑

在动态内存管理中,malloc是动态内存申请的一个最核心的函数,calloc是在malloc的基础上进行的空间内容的初始化,realloc是在malloc的基础上进行的空间大小的调整,因此我们可以认为free在释放空间时需要判断该空间是否是由malloc申请的有效空间。

那么这个有效空间具体指的是什么呢?

其实我们通过分析malloc的功能就能明白了——malloc是用来在堆区申请指定字节大小的内存空间。

这里的关键字就是堆区指定字节大小

  • 在C/C++程序中,堆区位于低地址处,栈区位于高地址处,两个分区所在的地址是不相同的,malloc在进行空间申请时,就是从堆区开始查找空间;
  • 当我们指定了开辟空间的大小后,malloc会根据该指定的字节大小进行精确查找,当堆区中没有指定大小的空余空间时,函数就会返回NULL,当存在该大小的空间时,函数就会返回该空间的起始地址;

malloc在申请内存时会记录申请的内存空间的大小,然后free在释放时会比较进行两次比较:

  1. 释放空间的地址是否在堆区;
  2. 需要释放的空间是否等于这个大小

通过这两次比较以此来判断该释放的空间是否为有效空间,当然,具体的判断过程我们不去深究,这里我们只需要知道free函数在释放空间时会判断释放空间的大小是否与申请的空间大小相同即可。因此当我们使用free来释放开辟空间的一部分时,就会程序就会报错,如下所示:

常见错误
因此为了避免这个问题的出现,大家在对申请好的空间进行访问时,一定得注意起始地址的记录,在进行空间释放时,一定是传入的空间起始地址;

2.5 未对开辟的空间进行释放导致内存泄漏

这个问题一般出现在使用完堆区的空间后,因各种因素而导致未及时释放空间,如下所示:

//常见错误5——未通过`free`释放空间
void test11() {
	//创建10个整型空间
	int* p = (int*)calloc(10, sizeof(int));
	//及时进行判空,避免错误1
	if (!p) {
		perror("calloc");
		return;
	}
}

在这个例子中可以看到,我们在完成空间申请后并未对该空间进行释放,函数就直接结束了。

对于堆区的空间而言,它可以由程序员通过free来主动释放内存,也可以在程序结束后通过操作系统自动回收内存,当遇到上例这种情况时,那空间的回收就只能够有操作系统来完成。

目前来看好像是每什么问题,接下来我们继续往下看:

常见错误2
但是现在我们可以看到,当我们在调用test11这个函数后,随即在主函数内又进行了内存空间的开辟,可以此时主函数内的空间开辟却失败了,这就是因为我们没有主动将test11函数中申请的空间释放,使得堆区中的内存被占用,从而影响了后续的使用;

因此为了避免出现这种问题,我们一定要记住,只要有进行内存空间的申请,那么就需要有一次对应的内存空间释放,如下所示:

常见错误3
那是不是说只要我们像这样处理就行了呢?下面我们继续往下看:

//常见错误5——未通过`free`释放空间
void test12() {
	//创建10个整型空间
	int* p = (int*)calloc(10, sizeof(int));
	//及时进行判空,避免错误1
	if (!p) {
		perror("calloc");
		return;
	}
	//通过realloc调整空间大小
	int* tmp = (int*)realloc(p, 20 * sizeof(int));
	if (!tmp) {
		perror("realloc");
		return;
	}
	p = tmp;
	free(p);
	p = NULL;
}

在这个例子我们先是通过calloc向堆区申请了一块空间,之后又通过realloc将这块空间进行了扩容,在这之后我们便通过free释放了该空间。

从整个过程来看,似乎不存在任何问题,接下来我们继续往下看:

常见错误4
从测试结果可以看到,此时我们在主函数中申请内存时同样失败了,这又是为什么呢?

细心的朋友已经发现问题所在了,没错就是realloc扩容失败的处理上。

在代码中,我们对扩容失败的处理是直接结束函数的运行,这时后面释放空间的过程压根就不会执行,正因为这样,所以先前通过calloc申请的内存空间仍未被释放,因此这也就影响了后续对内存空间的申请操作。

为了避免这种情况的出现,我们一定要注意,当我们执行了申请空间的操作后,在使用完空间后一定需要将空间归还给操作系统。

那对于这里的修改我们则可以在扩容失败后,先将calloc申请的空间进行释放后,再回到主函数,如下所示:

常见错误5
可以看到,此时程序的运行就不会有任何问题。当然除了上述的情况可能导致内存泄漏外,还有我们之前介绍过的,直接通过指向需要进行扩容的空间的指针来接收realloc的返回值,在扩容失败后,丢失原先空间的地址而导致内存泄漏,这里我就不再继续展开。我们接着往下看;

2.6 对同一块空间进行多次释放

这个问题常出现在多个指针指向同一块空间时的情况,如下所示:

//常见错误6——对同一块空间进行多次释放
void test13() {
	//创建5个整型空间
	int* p = (int*)calloc(5, sizeof(int));
	//及时进行判空,避免错误1
	if (!p) {
		perror("test13:calloc");
		return;
	}
	//通过realloc进行扩容
	int* tmp = (int*)realloc(p, 8 * sizeof(int));
	if (!tmp) {
		perror("test13:realloc");
		//扩容失败时,主动释放calloc申请的空间,避免错误5
		free(p);
		p = NULL;
		return;
	}
	free(tmp);
	tmp = NULL;
	free(p);
	p = NULL;
}

在这个例子中,我们通过calloc先申请了一块空间,随后通过realloc进行了扩容,为了避免内存泄漏,我们在函数返回前,通过free释放了内存空间,并且在完成扩容后,我们通过通过指针释放了内存空间,这样是不是就没问题了呢?下面我们就来测试一下:

常见错误6
可以看到,此时同样出现了报错,这又是为什么呢?

从输出的地址我们可以看到,指针p和指针tmp此时指向的是同一块空间,我们在完成扩容后,先是通过free释放了tmp指向的空间,随后又通过free释放了p指向的空间,正因为他们指向的是同一块空间,这就导致了这块空间被重复的进行了释放。

这个问题同样与free函数的底层逻辑有关,根据前面的介绍,我们知道free在进行空间释放时会判断该空间是否时有效空间,当第一次释放该空间时,这时能够释放成功是因为释放的空间为有效空间。

可是这里我们需要注意——空间一旦被释放,它就处于了可使用的状态,而通过malloc函数申请好的空间是出于不可用状态

因此free函数在第二次释放该空间时,会通过空间的可使用状态来判断该空间并不是有效空间。这也就是为什么当我们释放同一块空间时会出现报错。

为了避免这个问题的产生,我们需要注意以下几点:

  1. 一个内存申请的动作只能够匹配一次内存释放的动作;
  2. 通过realloc进行扩容成功时,我们只需要对realloc的返回值进行一次内存释放即可;

结语

今天的内容到这里就全部结束了,在下一篇内容中我们将介绍《柔性数组》的相关内容,大家记得关注哦!如果大家喜欢博主的内容,可以点赞、收藏加评论支持一下博主,当然也可以将博主的内容转发给你身边需要的朋友。最后感谢各位朋友的支持,咱们下一篇再见!!!

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

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

相关文章

【HarmonyOS】- 内存优化

文章目录 知识回顾前言源码分析1. onMemoryLevel2. 使用LRUCache优化ArkTS内存原理介绍3. 使用生命周期管理优化ArkTS内存4. 使用purgeable优化C++内存拓展知识1. Purgeable Memory总结知识回顾 前言 当应用程序占用过多内存时,系统可能会频繁进行内存回收和重新分配,导致应…

Excel文档的读取【2】

读取了工作簿对象后&#xff0c;下一步就是读取指定的工作表。每个工作簿中&#xff0c;都包含一个或多个工作表。每个工作表&#xff0c;都有一个名称。阿珍收到的Excel文件&#xff0c;由两个工作表组成&#xff0c;分别为“销售商品”和“销售订单数据”。 使用print输出…

基于移动互联网的校内物业报修管理系统设计与实现(论文+源码)_kaic

基于移动互联网的校内物业报修管理系统设计与实现 摘  要 校园后勤服务对于学校的发展至关重要&#xff0c;它不仅是学校管理的基石&#xff0c;也是实现教育目标的关键因素&#xff0c;为学生提供优质的生活环境。如果学校能够提供出色的后勤保障&#xff0c;让师生无需担心…

【生日视频制作】保时捷车主提车交车仪式感AE模板修改文字软件生成器教程特效素材【AE模板】

生日视频制作教程保时捷车主提车交车仪式感AE模板修改文字特效广告生成神器素材祝福玩法AE模板工程 怎么如何做的【生日视频制作】保时捷车主提车交车仪式感AE模板修改文字软件生成器教程特效素材【AE模板】 生日视频制作步骤&#xff1a; 下载AE模板 安装AE软件 把AE模板导入…

240908-Linux通过ln设置软链接关联大模型文件

在Linux中&#xff0c;您可以使用ln命令来创建软链接&#xff08;符号链接&#xff09;。软链接是一种特殊类型的文件&#xff0c;它指向另一个文件或目录。以下是如何设置软链接的步骤&#xff1a; 创建软链接 基本语法&#xff1a; ln -s [目标文件或目录] [软链接的名称]示…

【SpringBoot】电脑商城-12-订单功能

创建订单 1 订单-创建数据表 1.使用use命令先选中store数据库。 USE store;2.在store数据库中创建t_order和t_order_item数据表。 CREATE TABLE t_order (oid INT AUTO_INCREMENT COMMENT 订单id,uid INT NOT NULL COMMENT 用户id,recv_name VARCHAR(20) NOT NULL COMMENT …

碎碎念,只是一些关于人工智能的随笔记录。

&#x1f96e;随笔 本篇将毫无逻辑&#xff0c;只是写到哪儿算哪儿&#xff0c;只是用来记录着玩儿。 关键学习期的概念 关键学习期&#xff08;Critical Learning Periods&#xff09;是一个在生物学和人工智能领域都有研究的概念。在生物学中&#xff0c;关键学习期指的是…

CAN总线的位同步详细讲解

接收方数据采样 &#xff08;1&#xff09;CAN总线没有时钟线&#xff0c;总线上的所有设备通过约定波特率的方式确定每一个数据位的时长 &#xff08;2&#xff09;发送方以约定的位时长每隔固定时间输出一个数据位 &#xff08;3&#xff09;接收方以约定的位时长每隔固定…

数据结构与算法——Java实现 4.数组

目录 一、数组 — 概述 1.定义 2.特点 3.公式 小测试 二、数组的性能 1.空间占用 2.随机访问 三、动态数组 1.实现动态数组 2.新增元素&#xff08;最后一个元素位置&#xff09; 3.新增元素、数组扩容 4.检查数组容量 5.类中定义的其他方法 ① 按索引查找元素 ② 返回数组长度…

python例子:相片处理工具(可视化)

作品名称&#xff1a;相片处理工具&#xff08;可视化&#xff09; 开发环境&#xff1a;PyCharm 2023.3.4 python3.7 用到的库&#xff1a;sys、os、cv2、numpy、math和random 作品简介&#xff1a;运行例子后&#xff0c;先选择需要处理的图片&#xff0c;然后可对图片进…

鸿蒙开发中实现自定义弹窗 (CustomDialog)

效果图 #思路 创建带有 CustomDialog 修饰的组件 &#xff0c;并且在组件内部定义controller: CustomDialogController 实例化CustomDialogController&#xff0c;加载组件&#xff0c;open()-> 打开对话框 &#xff0c; close() -> 关闭对话框 #定义弹窗 (CustomDial…

利用Django框架快速构建Web应用:从零到上线

随着互联网的发展&#xff0c;Web应用的需求日益增长&#xff0c;而Django作为一个高级的Python Web框架&#xff0c;以其强大的功能和灵活的架构&#xff0c;成为了众多开发者的选择。本文将指导你如何从零开始使用Django框架构建一个简单的Web应用&#xff0c;并将其部署到线…

springboot学习(10)(自定义starter)(自定义mybatis的starter)(解决:java:错误:不支持发行版本?)

目录 一、引言 &#xff08;1&#xff09;为什么要学习自定义starter&#xff1f; &#xff08;2&#xff09;如何自定义starter&#xff1f; 二、制作一个自定义mybatis-starter &#xff08;1&#xff09;需求 &#xff08;2&#xff09;步骤 &#xff08;3&#xff09;开始实…

SQL各子句的执行顺序

FROM 、WHERE 、GROUP BY 、HAVING、 SELECT、ORDER BY 、LIMIT 执行顺序如下&#xff1a; FROM 子句&#xff1a;确定查询的基础表和视图。 WHERE 子句&#xff1a;基于指定的条件过滤记录。 GROUP BY子句&#xff1a;按指定的列分组结果集中的记录。 HAVING子句&#xff…

网上订餐新体验:SpringBoot技术驱动

1 绪论 1.1 研究背景 随着互联网技术的快速发展&#xff0c;网络时代的到来&#xff0c;网络信息也将会改变当今社会。各行各业在日常企业经营管理等方面也在慢慢的向规范化和网络化趋势汇合[13]。电子商务必将成为未来商务的主流&#xff0c;因此对于餐饮行业来说&#xff0c;…

Spring2~~~

注解配置Bean Spring的 IOC 容器检查到注解就会生成对象&#xff0c;但这个注解的具体含义不会识别 配置自动扫描的包 <!--配置容器要扫描的包1. component-scan 要对指定包下的类进行扫描, 并创建对象到容器2. base-package 指定要扫描的包3. 含义是当spring容器创建/初始…

类和对象(中)- C++

个人主页点这里~ 1. 类的默认成员函数 在C中&#xff0c;当你定义一个类时&#xff0c;即使没有显式地声明它们&#xff0c;编译器也会为该类生成一些默认成员函数。这些默认成员函数在特定情况下非常有用&#xff0c;尤其是当你进行对象拷贝、赋值、销毁或动态内存管理时。 …

BatchUpdateException 问题

我们在使用 statement 的 batch api 时、每 20 条数据作为一个事务提交、如果其中一条数据有问题 我们想知道哪一条数据有问题、那么我们可以通过捕获的 BatchUpdatedException 获取到 我们插入 100 条数据、第 21 条和第 41 条是非法数据、会插入失败的 看看日志打印 看看…

函数指针学习

认识函数指针&#xff1a; 函数指针的用处&#xff1a; 回调函数&#xff08;以函数指针为参数的参数&#xff09; 定义带标签的结构体 typedef struct mode { int data; } Node; 标签&#xff08;mode&#xff09;: mode 是结构体的标签名。在这种定义中&#xff0c;mo…

前端:Vue3学习-2

前端:Vue3学习-2 1. vue3 新特性-defineOptions2. vue3 新特性-defineModel3. vue3 Pinia-状态管理工具4. Pinia 持久化插件 -> pinia-plugin-persistedstate 1. vue3 新特性-defineOptions 如果要定义组件的name或其他自定义的属性&#xff0c;还是得回归原始得方法----再…