【嵌入式开发学习】__单片机中容易造成内存泄露的几个痛点

news2024/12/25 14:11:51

目录

前言

一、程序运行

 二、什么是内存泄露?

三、内存泄露的严重后果!

四、如何定位到泄露的要点? 

五、三大痛点

1. 访问越界

2. 栈

3. 堆

六、泄露常见的场景

1. 重新赋值

2. 首先释放父块

3. 返回值的不正确处理

七、常见的其他错误

1. 未初始化内存

2. 内存覆盖

3. 内存读取越界

(* ̄︶ ̄)创作不易!期待你们的 点赞、收藏和评论喔。


前言

指针、数组和内存分配,都是需要我们时刻关注的问题。为了防止程序在以上的地方出现BUG,我们需要养成良好的编程习惯。

嵌入式中,程序一般是存储在FLASH中,但是运行的时候是在内存(RAM)里运行的。

一、程序运行

下面的Demo程序在运行的时候,大概都会在内存中的哪些地方呢?

#define OFF 0x00          	//宏定义不占用内存空间,宏在预处理阶段会被替换掉,执							行文件中并不存在宏定义.
 float Num = 3.14;         	//全局变量,存在于“变量区”
 char* str = NULL;         		//全局变量,存在于“变量区”
 int main()
 {
int time;               	//局部变量,存在于“栈区”
char* path = "C:\\Users";   	//字符串常量,存在于“常量区”
int Users_Num[8];       	//局部变量,存在于“栈区”
srt = (char*)malloc(10);    	//申请动态存储区,存储于“堆区”
static flaot con = 1.0;   		//静态局部变量,存在于“变量区”
     
//程序代码区
delay_ms(time);         	//time未初始化,该变量为垃圾值
strcap(str,"Hello World");	//字符串“hello World”比str申请的内存大,造成访问越界
free(str);                 //释放str内存
str = NULL;               //使str指针指向空地址,方便下次使用
 }

 二、什么是内存泄露?

内存泄漏是指你向系统申请分配内存进行使用(new / malloc),然后系统在堆内存中给这个对象申请一块内存空间,但当我们使用完了却没有归系统(delete),导致这个不使用的对象一直占据内存单元,造成系统将不能再把它分配给需要的程序。

三、内存泄露的严重后果!

  1. 程序在运行后,时间的累计导致占用更多的内存,最后无内存可用而造成程序崩溃;
  2. 程序消耗了大量内存,导致其他程序不能正常运行;
  3. 性能下降;
  4. 程序不达标准;

四、如何定位到泄露的要点? 

  1. 根据原理,review 自己的代码,利用开发工具的查找功能,查找或者删除,看看内存的申请与释放是不是成对释放的,这使你迅速发现一些逻辑较为简单的内存泄露情况。
  2. 如果依旧发生内存泄露,可以通过记录申请与释放的对象数目是否一致来判断。在类中追加一个静态变量 static int count;在构造函数中执行count++;在析构函数中执行count--;通过在程序结束前将所有类析构,之后输出静态变量,看count的值是否为0,如果为0,则问题并非出现在该处,如果不为0,则是该类型对象没有完全释放。
  3. 检查类中申请的空间是否完全释放,尤其是存在继承父类的情况,看看子类中是否调用了父类的析构函数,有可能会因为子类析构时没有是否父类中申请的内存空间。
  4. 对于函数中申请的临时空间,认真检查,是否存在提前跳出函数的地方没有释放内存。

五、三大痛点

1. 访问越界

Demo程序上的 strcap() 语句出现后面字符串大小比前面 str 变量申请的空间内存大小大的情况就会出现访问越界的情况。写程序时尤其要关注那个字符串它到底有多长,一定要去留意一下。如果出现访问越界的问题有些编译器是可能识别不出来的,但确实会造成这个内存访问错误,除此之外还有一些类似的:比如像 sprintf()strcat() 等函数都有可能会导致访问越界的情况发生。

还有就是数组,数组也是特别容易造成访问越界的,有些编译器可能会检测数组长度是否超出数组下标长度,但有的地方未必就能检测出,如将数组在 for() 循环里访问的,这个时候就需要注意了,千万小心不要让它出现访问越界的情况。因为编译是检测不出来的,但是在运行的时候就会出现内存访问的故障了。

2. 栈

根据上面Demo程序写的我们知道局部变量是存在于“栈区”的,所以一般我们的局部变量通常不要定义的太大,尤其是一些数组变量,如果说非常大,就会占用非常大的栈区空间,那么这在程序运行的时候非常容易出现栈溢出。平常我们程序里不可避免的会调用一些函数,所以我们调用一些函数的时候最好不要有深层次的调用,因为在调用函数的过程中栈区会不停的存储函数相关的一些变量和一些地址。所以需要深层次的函数递归调用的时候,大家尽量采用别的方式去代替。

3. 堆

当申请了动态区域,用完的时候一定要记得释放(free),如果没有释放,那么这块内存区域就将处于不可用状态(就像占着茅坑不拉屎一样),程序大了或运行久了就极有可能会导致内存的泄露(重启一下就能解决90%的问题根源),同时我们在释放的时候也要注意释放的内存只能释放一次,不要重复的释放,有的时候代码量会比较大,所以有可能会在不止一处地方进行了代码的释放操作。因为我们内存释放了一次后,该内存区域就有可能用来做别的事了,如果这时候我们又再释放一遍就很有可能会出现问题了。释放完之后最好把指针指向空地址,避免下次再使用指针的时候出现地址的错误。

六、泄露常见的场景

1. 重新赋值

char *memoryArea = malloc(10);
char *newArea = malloc(10);

memoryAreanewArea 分别被分配了 10 个字节,它们各自的内容如图 4 所示。如果某人执行如下所示的语句(指针重新赋值)……

memoryArea = newArea;

则它肯定会在该模块开发的后续阶段给您带来麻烦。

在上面的代码语句中,开发人员将 memoryArea 指针赋值给 newArea 指针。结果,memoryArea 以前所指向的内存位置变成了孤立的,如下面的图5所示。它无法释放,因为没有指向该位置的引用。这会导致 10 个字节的内存泄漏。

在对指针赋值前,请确保内存位置不会变为孤立的

2. 首先释放父块

假设有一个指针 memoryArea,它指向一个 10 字节的内存位置。该内存位置的第三个字节又指向某个动态分配的 10 字节的内存位置,如下图所示。

free(memoryArea)

如果通过调用 free 来释放了 memoryArea,则 newArea 指针也会因此而变得无效。newArea 以前所指向的内存位置无法释放,因为已经没有指向该位置的指针。换句话说,newArea 所指向的内存位置变为了孤立的,从而导致了内存泄漏。

每当释放结构化的元素,而该元素又包含指向动态分配的内存位置的指针时,应首先遍历子内存位置(在此例中为 newArea),并从那里开始释放,然后再遍历回父节点。

这里的正确实现应该为:

free( memoryArea->newArea);

free(memoryArea);

3. 返回值的不正确处理

有时,某些函数会返回对动态分配的内存的引用。跟踪该内存位置并正确地处理它就成为了 calling 函数的职责。

char *func ( )
{
	return malloc(20); // make sure to memset this location to ‘\0’…
}
void callingFunc ( )
{
	func ( ); // Problem lies here
}

在上面的示例中,callingFunc() 函数中对 func() 函数的调用未处理该内存位置的返回地址。结果,func() 函数所分配的 20 个字节的块就丢失了,并导致了内存泄漏。 

七、常见的其他错误

1. 未初始化内存

T

H

I

S

I

S

M

I

N

E

char *p = malloc ( 10 );

在这一段代码的时候,p 已被分配了 10 个字节。这 10 个字节可能包含垃圾数据

如果在对这个 p 赋值前,某个代码段尝试访问它,则可能会获得垃圾值,您的程序可能具有不可预测的行为。p 可能具有您的程序从未曾预料到的值

良好的习惯是始终结合使用 memsetmalloc分配内存,或者使用 calloc

char *p = malloc (10);

memset(p,’\0’,10);

现在,即使同一个代码段尝试在对 p 赋值前访问它,该代码段也能正确处理 Null 值(在理想情况下应具有的值),然后将具有正确的行为。

2. 内存覆盖

由于 p 已被分配了 10 个字节,如果某个代码片段尝试向 p 写入一个 11 字节的值,则该操作将在不告诉您的情况下自动从其他某个位置“吃掉”一个字节。让我们假设指针 q 表示该内存。

I

M

Q

原始q的内容:

&

M

Q

覆盖后q的内容

结果,指针 q 将具有从未预料到的内容。即使您的模块编码得足够好,也可能由于某个共存模块执行某些内存操作而具有不正确的行为。下面的示例代码片段也可以说明这种场景。

char *name = (char *) malloc(11);

// Assign some value to name

memcpy ( p,name,11); // Problem begins here

在本例中,memcpy 操作尝试将 11 个字节写到 p,而后者仅被分配了 10 个字节。

作为良好的实践,每当向指针写入值时,都要确保对可用字节数和所写入的字节数进行交叉核对。一般情况下,memcpy 函数将是用于此目的的检查点

3. 内存读取越界

内存读取越界 (overread) 是指所读取的字节数多于它们应有的字节数。这个问题并不太严重,在此就不再详述了。下面的代码提供了一个示例。

char *ptr = (char *)malloc(10);

char name[20] ;

memcpy ( name,ptr,20); // Problem begins here

在本例中,memcpy 操作尝试从 ptr 读取 20 个字节,但是后者仅被分配了 10 个字节。这还会导致不希望的输出。


(* ̄︶ ̄)创作不易!期待你们的 点赞收藏评论喔。

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

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

相关文章

无监督学习-K-means

1、 什么是无监督学习 一家广告平台需要根据相似的人口学特征和购买习惯将美国人口分成不同的小组,以便广告客户可以通过有关联的广告接触到他们的目标客户。Airbnb 需要将自己的房屋清单分组成不同的社区,以便用户能更轻松地查阅这些清单。一个数据科学…

账号运营的底层逻辑---获客思维

什么是运营? 运营是做什么的? 什么是内容运营? 什么是活动运营? 一篇带你搞清楚所有的底层逻辑!

三、W5100S/W5500+RP2040树莓派Pico<TCP Client数据回环测试>

文章目录 1. 前言2. 协议简介2.1 简述2.2 优点2.3 应用 3. WIZnet以太网芯片4. TCP Client数据回环测试4.1 程序流程图4.2 测试准备4.3 连接方式4.4 相关代码4.5 测试现象 5. 注意事项6. 相关链接 1. 前言 在当今的计算机网络环境中,TCP(传输控制协议&am…

整理指定文件夹下的所有文件,以类树状图显示并生成对应超链接

最近在整理家里学习资料的时候,由于年代久远,找不到我想要找的文件,windows文件搜索速度感觉太慢。于是想要生成一份类似文件索引的东西来显示所有资料,让我可以快速的找到需要的资料路径 直接上代码 import os import datetim…

基于Java的医院远程预约管理系统设计与实现(源码+lw+部署文档+讲解等)

文章目录 前言具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序(小蔡coding) 代码参考数据库参考源码获取 前言 💗博主介绍:✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作者&am…

2、Linux权限理解

个人主页:Lei宝啊 愿所有美好如期而遇 目录 前言 Linux权限的概念 1.文件访问者的分(人) 2.文件类型和访问权限(事物属性) 3.文件权限值的表示方法 4.文件访问权限的相关设置方法 file指令 目录的权限 粘滞位 关于权限的总结 前言 在开始Linux权限理…

IDEA: 自用主题及字体搭配推荐

文章目录 1. 字体设置推荐2. 主题推荐3. Rainbow Brackets(彩虹括号)4. 设置背景图片 下面是我的 IDEA 主题和字体,它们的搭配效果如下: 1. 字体设置推荐 在使用 IntelliJ IDEA 进行编码和开发时,一个合适的字体设置可以提高你的工作效率和舒…

Python---for循环中的两大关键字break和continue

之前在while循环中,也是用到两个关键字。 相关链接: 所以,在循环结构中都存在两个关键字:break和continue break:主要功能是终止整个循环 break:代表终止整个循环结构 continue:代表中止当…

【Note】二叉树的遍历

二叉树的遍历 二叉树的基本结构:根节点(Data)、左子树(LChild)和右子树(RChild)。 因此只要依次遍历这三部分,就遍历了整个二叉树。 如果用L、D、R分别表示遍历左子树、访问根结点…

不用编程超简单的自动化测试工具:Airtest入门篇教程!

目录 一、背景 二、什么是Airtest 三、Airtest下载安装 四、Airtest入门使用教程 4.1 连接设备: 4.2 具体测试场景案例: 五、总结 一、背景 很多刚入行或从其他行业转行做测试的同学,日复一日每天做点工已经点得疲惫和麻木&#xff0…

SparkSQL执行流程与Catalyst优化器

目录 一、SparkSQL运行流程与Catalyst优化器 (1)RDD运行流程 (2)SparkSQL自动优化 (3)Catalyst优化器流程 (4)Catalyst优化器总结 (5)Spark SQL执行流程 一、…

Leo赠书活动-02期 【信息科技风险管理:合规管理、技术防控与数字化】

✅作者简介:大家好,我是Leo,热爱Java后端开发者,一个想要与大家共同进步的男人😉😉 🍎个人主页:Leo的博客 💞当前专栏: 赠书活动专栏 ✨特色专栏:…

五、W5100S/W5500+RP2040树莓派Pico<UDP Client数据回环测试>

文章目录 1. 前言2. 协议简介2.1 简述2.2 优点2.3 应用 3. WIZnet以太网芯片4. UDP Client回环测试4.1 程序流程图4.2 测试准备4.3 连接方式4.4 相关代码4.5 测试现象 5. 注意事项6. 相关链接 1. 前言 UDP是一种无连接的网络协议,它提供了一种简单的、不可靠的方式来…

Premiere Pro(Pr)2023软件下载及安装教程

目录 一.简介 二.安装步骤 软件:Pr版本:2023语言:简体中文大小:8.30G安装环境:Win11/Win10(1809版本以上)硬件要求:CPU2.6GHz 内存8G(或更高,不支持7代以下CPU&#xf…

Android系统启动

首语 Android系统启动与应用启动、四大组件、AMS等很多内容都有关联,因此,Android系统启动是首先需要了解的知识。 Android 系统启动流程 Android系统流程主要部分如上图所示。下面对各个流程进行解析。 Boot ROM 启动电源以及系统启动。当电源按下时…

启动1000万个虚拟线程需要多少时间?需要多少平台线程?

之前,在Java新特性专栏中,我们简单介绍了Java 21正式发布的虚拟线程。 昨天,正好看到一个讲解此内容的视频,非常不错,所以DD这里给大家翻译好了,感兴趣的可以看看。可以进一步了解虚拟线程。 什么是虚拟线…

二进制搭建 Kubernetes+部署网络组件+部署CornDNS+负载均衡部署+部署Dashboard

二进制搭建 Kubernetes v1.20 k8s集群master01:20.0.0.50 kube-apiserver kube-controller-manager kube-scheduler etcd k8s集群master02:20.0.0.100k8s集群node01:20.0.0.110 kubelet kube-proxy docker etcd k8s集群node02:20.…

ArcGIS笔记12_ArcGIS搜索工具没法用?ArcGIS运行很慢很卡?

本文目录 前言Step 1 ArcGIS搜索工具没法用Step 2 ArcGIS运行很慢很卡 前言 这是笔者最近遇到的两个小问题,新换了台式机,安装上ArcGIS后发现搜索工具没法用,而且感觉还不如原来笔记本运行的流畅,加载图层很慢,编辑要…

基于YOLOv8模型和UA-DETRAC数据集的车辆目标检测系统(PyTorch+Pyside6+YOLOv8模型)

摘要:基于YOLOv8模型和UA-DETRAC数据集的车辆目标检测系统可用于日常生活中检测与定位汽车(car)、公共汽车(bus)、面包车(vans)等目标,利用深度学习算法可实现图片、视频、摄像头等方…