总结了一些内存问题

本文详细介绍了C语言中的内存模块分区,包括栈区、堆区、静态区、常量区和代码区,并深入讲解了内存问题,如内存越界、多重定义、内存泄漏、指针错误(未初始化的指针、野指针、悬空指针)和堆栈溢出。内存泄漏可能导致程序性能下降甚至崩溃,而指针错误中的野指针和悬空指针也是常见的错误。此外,文章还讨论了内存碎片及其影响。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

内存问题
了解内存问题之前 首先 需要知道C语言中的内存模块分区。
C语言中的内存模块分区:
C语言的内存模型分为5个区:栈区、堆区、静态区、常量区、代码区。每个区存储的内容如下:

1、栈区:存放函数的参数值、局部变量等,由编译器自动分配和释放,通常在函数执行完后就释放了,其操作方式类似于数据结构中的栈。栈内存分配运算内置于CPU的指令集,效率很高,但是分配的内存量有限,比如iOS中栈区的大小是2M。(栈是先进后出,队列是先进先出)

2、堆区:就是通过new、malloc、realloc分配的内存块,编译器不会负责它们的释放工作,需要用程序区释放。即 一般需要程序员手动申请和释放。分配方式类似于数据结构中的链表。“内存泄漏”通常说的就是堆区。

3、静态区:全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后,由系统释放。

4、常量区:常量存储在这里,不允许修改。(其实常量,全局变量和静态变量 都可统一称为全局区)

5、代码区:顾名思义,存放代码,只读区域,即在程序运行中无法做任何修改的存储区域。(其实可读可写的区域也都称为数据区)

内存问题 C语言中的内存模块分区:

内存越界


发生场景 :一般在数据区,一般有两种情形:读越界和写越界。

1.读越界:
表示 读取到了不属于有效范围(或合法范围)的区域的数据。
例如:

以上两种,下标范围是(0-4),一个是读取到了初始下标之前为-1 的不属于它的区域,一个是读取到了最终下标的 下标为5的区域, 理论上说 ,最终都会造成程序的崩溃。但是实际上在VC6.0中 程序继续没有报错,但是返回值 是一个根据你数组上下文的数据的一个随机值。完全不是你应该读取到的正常值

2.写入越界

数据写入不属于有效范围(或合法范围)的区域,写越界一般也称为“缓存区溢出”,所写入数据对于目标地址而言是随机的。

以上则是 写入超过有效范围的数据, 可以看出 读取的时候 也超过了有效范围,导致超过有效范围(下标0-4)的数据 输入与输出的时候是有差异的。 一般来说,超界的时候会造成程序崩溃 但是在vc6.0中没有报错 程序正常执行,但是最终结果是随机值。

可以看出输入超界后 ,有效范围内的数据读取的时候 数据是正常的。所以可知,其实写入越界是一种隐性的故障,虽然有时表面看似没有问题,但会影响程序的稳定性和造成后续的故障。
一般在sprintf ,strcpy,strcat,gets时 都应该注意内存越界问题,尽量在定义内存时,进行合理的定义可以有效避免此类问题

多重定义:

在定义数据时 ,已经定义过后,又进行了二次乃次多次的重复 定义,叫做多重定义。

在此处错误:
a 被 定义且初始化赋值后,又进行了一次定义 ,所以报错的是redefinitionS重复定义,而且在被多重定义后,又进行了一次赋值 ,所以又报错了多重初始化的问题.

如果是只进行多重定义的话,其实只会报错一个多重定义问题,所以需要注意:有时候在查找问题时,会发现很多问题时连带的,是因为开头的错误,会导致后面出现一系列的连带错误。

*以下是正常的定义:

正常的定义: 一般定义在开头,且每个名称定义一次即可;
但是 所定义的内存的名称可以进行多次赋值;

内存泄漏:

一般发生在堆区,本质是 有已经申请或存在的内存空间,但是被孤立,导致内存空间空闲,没有释放或无法使用。
图例:

此处 p ,q都开辟了一个 int 10的空间,但是当p=q 的时候,则代表 p的指针指针指向了q的内存空间,导致 p的内存空间闲置, 即使后面进行了free§,但是释放的是q的内存空间,p原来的内存空间被孤立了 无法被释放。导致了内存泄漏

当错误的内存释放,也会发生内存泄漏.

此处p开辟了10个空间 但是np指向了p从三个开始的空间的十个空间,在p空间被释放后,np指向的p的空间也被关闭了,但是剩余的空间却被孤立出来,导致了内存的泄漏;

(还有就是:在内存分配后忘记使用 free 进行释放,一直存在)

而内存泄漏按照泄漏的频率一般分为4类:
1.经常性泄漏,即能发生泄漏的代码被多次执行,导致每次执行都在进行内存泄漏。
2.偶尔性泄漏 ,指只在特定的情况或环境下才会发生的内存泄漏。但是如果一直处于此环境或情况下,偶尔性泄漏也会变成经常性泄漏
3.一次性内存泄漏,指 发生泄漏的内存只执行一次。
4.隐式内存泄漏,指 在程序运行时,不断地分配内存,直到程序结束
内存泄漏的危害:
少量的内存泄漏现象并不明显,但是如果不断地发生内存泄漏会导致 程序和设备的性能越来越差,越来越卡,直到程序崩溃或者部分或者全部设备停止工作以及提示系统内存耗尽,影响程序和设备的正常运行。

***但是我们电脑一般会有保护机制,在内存泄漏到一个峰值的时候,不再自动进行开辟空间,所以会发现下面的内存会突然提高的一个峰值然后稳定在这个峰值.

所以: 一般开辟使用完内存后,必须进行及时的释放和置空.

指针错误:

未初始化的指针:

即定义指针的时候未初始化,导致指针默认为随机值,指针指向了随机的地址;

运行后 程序会报错 指针p未初始化。

所以我们在进行 指针定义时 也需要给指针进行初始化,如果需要给指针赋值的话 也需要指向一个申请的内存空间。如:以下

指针在定义时进行了初始化 和 指向了a的内存空间 ,然后对指针p的空间内存储的值进行修改,我们得到了最后的输出 10
野指针:
即:指针 指向了不可知的位置或者内存空间

出现场景:

1**.指针未初始化时,参考上面未初始化指针情况。**
需要注意:开辟空间时,指针使用之前检查有效性 (例如:判断是否为NULL)
2.指针越界访问,类似于 越界访问的问题,但是将数组换成了指针

在有效范围内进行操作,即可避免此问题
3.指针释放后未置空.

在此指针指向了局部变量中i,但是在局部变量运行完后,i空间已被释放,但是指针p未置空,导致指针p仍让指向了该内存的位置(即悬空指针),指针p值因为未置空仍然存在,而进行使用它的值的时候,造成了野指针.

*** 在释放空间后,指针p也得进行置空,则可避免此种野指针情况出现

悬空指针
指 在释放后仍然指向该内存位置的指针 或者我们可以说是指针,它没有指向适当类型的有效数据对象。 悬空指针指向的存储位置称为悬空引用。
如图:
释放后 P未置空,仍然存在有值时,此时P称之为悬空指针

引用空指针:
空指针 : 即由系统保证不指向任何对象或函数的指针,称为空指针。
** 也可以理解为 任何指向函数或对象地址的指针 都不能被称为空指针;
例如:

int a = 6;
int * p = null;
int * p = &a;

此时指针p就会报错,因为引用了空指针;

空类型指针: void * (又叫万能指针)
万能指针,就是该指针能接收任意类型的指针,可以指向任何类型对象,所以不能对空类型指针进行解引用,必须强制类型转换成相应的指针类型,才能进行解引用操作。
例如: 作为qsort中的参数引用
空指针类型:
作为函数形参类型,可以接收任意类型的指针;
作为函数返回值类型,在函数外面,将其强制类型转换为相应的指针类型
可以与另一个void*类型指针比较大小

二次释放:
顾名思义: 就是进行二次乃至多次的释放

int a = 6;
int * p = &a;
free(p);
free(p);

导致结果:

所以一般进行相应的释放即可,不能对同一个内存空间进行二次甚至多次释放
段错误
程序试图访问不允许访问的内存位置,或试图以不允许的方式访问内存位置(例如尝试写入只读位置,或覆盖部分操作系统)时会发生段错误

1.访问了不允许访问的地址(如已经释放的指针地址)

堆栈溢出

堆栈溢出: 一旦程序确定,堆栈内存空间的大小就是固定的,当数据已经把堆栈的空间占满时,再继续进行存放则会发生溢出
原因:

1.函数调用层次太深。递归超过100层;
int s=1000;
func(s);
void func(int s)
{
if(s==1) return 1;
func(s-1);
}
2.自动变量过大:
即函数自身未定义过大的自动变量,但是在调用库函数或第三方接口时,使用了堆栈过大的空间,以至于调用第三方的自动变量大过函数自身定义的范围导致了堆栈溢出
3.动态使用空间后未进行释放
char * p = NULL;
4.堆栈数组访问越界
C语言数组是静态的,不能自动扩容,当下标小于零或大于等于数组长度时,就发生了越界(Out Of Bounds),访问到数组以外的内存。如果下标小于零,就会发生下限越界(Off Normal Lower);如果下标大于等于数组长度,就会发生上限越界(Off Normal Upper)。
即:堆栈设置的空间范围固定后,而输入数目或者数值过大或者下标范围过小过大,越界导致堆栈溢出;

可以参考内存越界访问内容
例如:数组访问越界
char s[5]=“Hello”;
char * p = s;
strcpy(p,“ABCDEFG”);
在此类中,就是 输入的元素个数大于了定义的堆栈空间的内存范围,导致发生越界。

对于堆栈溢出的防范措施有:
(1) 强制按照正确的规则写代码
(2) 通过操作系统使得缓冲区不可执行,从而阻止攻击者植入攻击代码。但由于攻击者并不一定要通过植入代码来实现攻击,同时linux在信号传递和GCC的在线重用都使用了可执行堆栈的属性,因此该方法依然有一定弱点。
(3) 利用编译器的边界检查来实现缓冲区的保护。该方法使得缓冲区溢出不可能出现,完全消除了缓冲区溢出的威胁,但代价较大,如性能速度变慢。
(4) 程序指针完整性检查,该方法能阻止绝大多数缓冲区溢出攻击。该方法就是说在程序使用指针之前,检查指针的内容是否发生了变化

内存碎片

内部碎片


顾名思义:就是在运行过程中内部存在的内存碎片;
原理:
由于申请的时候申请的不是4字节的倍数造成的,例如申请了17个字节,但是系统不会给你分配17个而可能是20个字节,这就造成了内部碎片的产生。

解决办法:
尽量申请4的倍数的字节,或者对于大内存的单片机来说,可以规定申请的单位是k,就可以避免这个问题。

外部碎片:

因为内存的申请一般都是连续的,但是当出现以下情况时,就会发生内存被孤立不连续的情况,从而造成外部碎片。例如
a申请了 10k,用完没有释放,b申请了5k,a这时释放了10k,c需要用15k,由于b没有释放,只能向后申请(15k之后的空间),只要以后申请的内存大于10k,那么a这10k就永远用不上
就是外部碎片。

并且程序运行时间长了,碎片积攒越来越多,导致系统崩溃。
解决办法:
a、内存用完立马释放,没有释放前不要申请新的内存(这种方法有局限性,不适应函数层层嵌套的情况)。
b、先申请的最后释放,不要在中途释放。
c、类似RTOS重写内存管理函数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值