内存分配方式全解析:静态、栈与堆,你了解多少?

检测维修 0 110

一、关于内存

1、内存分配方式

内存分配方式有三种:

内存从固定区域获取,程序编译阶段便已确定位置,该内存区域在程序执行过程中始终有效。

例如全局变量,static变量。

函数运行期间,其内部局部变量的空间位置通常在栈上生成,当函数运行终止后,这些空间位置会随即被自动回收。栈内存空间的分配机制是处理器指令系统的一部分,因而操作效率相当出色,不过可供分配的内存空间大小通常比较受限制。

内存分配于堆区,即为动态内存分配。程序执行期间,可通过malloc或new获取任意数量的存储空间,开发者需自行掌控何时运用free或delete来回收内存。这种内存管理模式下,存活时间由我们掌控,操作灵活多变,然而也容易引发诸多问题。

内存操作失误导致内存问题相当棘手。系统开发者无法自动识别此类失误,多数情况需在软件执行期间才能察觉。此类失误往往症状不显著,反复出现,加大了排错难度。不乏用户带着怒气前来投诉,但程序检查时却一切正常,而问题在工作人员离开后再次显现。下面列举常见的内存操作失误及其处理方法。

* 内存分配未成功,却使用了它。

编程初学者经常出现这类失误,由于他们往往忽略了内存分配可能失败的情况。普遍的应对策略是,在动用内存资源之前确认指针的值是否为零。倘若是通过malloc函数或new操作符来获取内存,务必采用if(指针值等于零)或者if(指针值不等于零)这样的语句来规避潜在风险。

* 内存分配虽然成功,但是尚未初始化就引用它。

这种错误的发生存在两个根本原因,首先在于缺乏初始化的意识,其次是对内存默认值的错误认知,认为其必然为零,从而造成引用初始值出现偏差,特别是针对数组的情况。内存的初始状态究竟如何并没有公认的规定,即便在某些情形下确实为零,我们也不应盲目假设。因此,不论采用何种方法构建数组,都必须进行初始化操作,即便是赋予零值也绝不能忽略,切莫以繁琐为由而省略。

* 内存分配成功并且已经初始化,但操作越过了内存的边界。

处理数组时常出现索引偏移一位的情况,有时会多出,有时会少掉。在for循环语句里,很容易弄错重复的次数,从而造成数组访问时超出范围。

* 忘记了释放内存,造成内存泄露。

出现这类问题的函数每次执行都会损耗一部分内存资源。起初系统内存尚有富余,因此不易察觉异常。最终某个时刻程序会 abrupt 停止运行,系统会弹出内存不足的警告信息。

动态内存分配和回收要成对进行,代码里malloc函数调用和free函数调用的数量必须相等,不然肯定出问题(new操作符和delete操作符也是这样)。

* 释放了内存却继续使用它。

有三种情况:

程序中的对象相互调用关系极为错综,十分难以弄清某个对象是否已经清除了内存空间,这种状况下需要重新规划数据构造,从源头上处理对象管理的混乱情形。

函数的返回语句存在错误,需要留意,不能返回指向“栈内存”的“指针”或“引用”,由于该内存会在函数体执行完毕时自动被销毁。

(3)在运用free或delete来释放内存资源之后,未能将指针指向NULL地址,因而造成了“野指针”现象。

分配内存后,必须马上验证指针是否为空,以防误用空指针。

必须记得为数组和动态内存设定初始值,以免将未初始化的存储空间当作右值来运用。

数组或指针的索引值不可超出其有效范围,需特别注意防止出现“超出预定范围1个单位”或“不足预定范围1个单位”的情况。

「规则4」动态内存的申请与释放必须配对,防止内存泄漏。

「规则五」当通过free或delete释放内存时,必须立刻将指针改写为NULL,以此避免出现无法访问的指针。

二、详解new,malloc,GlobalAlloc

1.new

new和delete运算符用于动态分配和撤销内存的运算符

new用法:

1>开辟单变量地址空间

创建一个整型动态数组,分配内存空间,获取该内存的首地址,将这个地址赋给整型指针变量,这个指针变量就指向了新开辟的内存区域。

a指向的内存单元被设置为5

2>开辟数组空间

一个整型指针a指向新分配的内存,该内存空间用于存放一个整数元素;接着,动态创建了一块能够容纳100个整数的连续存储区域;这块区域的起始地址被赋值给了变量a;

一般用法: new 类型

delete用法:

1> int *a = new int;

delete a; //释放单个int的空间

2>int *a = new int;

内存使用错误_内存分配方式_vc 内存泄漏检测工具

delete a; //释放int数组空间

想要进入new创建的内存块,不能直接用变量名,必须借助存放地址的指针,才能进行操作。

通过new和delete能够动态地分配,并释放内存区域。编程过程中,当某个变量(通常是临时存储的数组)使用完毕,下次又需要使用它,但不想再经历初始化的步骤时,可以在每次开始使用前分配一块空间,在用完后将其释放。

malloc函数的声明为extern void *malloc(unsigned int num_bytes);该函数用于申请内存空间,调用时需包含标准库头文件或特定头文件。它的作用是开辟一块大小等于num_bytes字节的连续内存区域。若申请成功,函数会给出指向这块内存的地址,否则会返回NULL指针。已分配的内存空间,在不再需要时应通过free函数来归还系统。

动态内存分配的格式如下,指针标识符等于数据类别指针类型动态内存分配函数,括号内为所需分配的内存字节数,括号外指定了数据类型,括号内的表达式代表分配的字节大小。

malloc用于向系统申请内存,能够分配指定字节数的空间。它的返回值是void*类型,这种类型代表不确定数据类型的指针。在C和C++语言中,void*类型支持强制转换成其他任何类型的指针。

malloc函数的运作方式在于,它维护一个由所有空闲内存区域组成的有序集合。当需要分配内存时,函数会遍历这个集合,寻找第一个容量满足需求的区域。找到后,会将该区域分割成两部分,一部分按需分配给用户,另一部分则保留为新的空闲区域。随后,要把用户得到的内存部分交给用户使用,同时把剩余的部分,假如还存在的话,再放回可供分配的列表中去,free函数被调用时,用户交还的内存部分会被加到空闲列表里,空闲列表最终会被分割成许多小块,万一这时用户需要一块较大的内存,空闲列表里可能就找不到能符合用户需求的连续区域了。接下来,malloc函数需要暂停执行,然后去空闲列表里仔细查找每一个内存区域,对它们进行归类,把挨着的小空闲部分拼接成更大的内存区域。

通过函数声明方式可以分辨出 malloc 和 new 的差异。new 会给出目标类型的指针,并且能自动推算出所需空间。定义一个整数型指针变量,然后使用关键字动态分配内存,该操作会返回一个指向整数的地址,同时系统会自动计算并分配所需内存空间的大小,具体为整型变量所占用的字节数;对于数组类型的指针,采用同样的方式,分配内存时系统会根据数组元素数量乘以单个元素大小来确定总分配空间,例如分配一百个整数的内存;与之相对,手动分配内存时需要预先精确计算所需字节数,并在获取内存块后,必须进行强制类型转换,将通用指针转换为特定数据类型的指针。

指针p声明为整型指针;p指向动态分配的整型内存;malloc函数返回的是通用指针类型;若直接赋值p = malloc(sizeof(int));则编译器会报错;错误信息为“不能将通用指针类型赋值给整型指针变量”;必须进行类型转换;正确写法为p = (int *)malloc(sizeof(int));所以必须通过 (int *) 来将强制转换。

第二,函数的参数为 sizeof(int),表示一个整数类型的数据所占用的存储空间。若编写 int* p = (int *) malloc (1);这样的代码,虽然编译器会允许,但实际上只分配了1字节的内存区域。当向该区域存放一个整数时,就会有3字节的数据无处安放,导致它们挤占了邻近的内存位置。造成的结果是后面的内存中原有数据内容全部被清空。

3. GlobalAlloc

在VC环境下,首先使用GlobalAlloc函数来申请一段内存空间,该函数会给出所申请内存的标识。接着调用GlobalLock函数来获取这块内存的访问权限,此函数需要一个内存标识作为输入,并会提供指向该内存区域的地址。您能够借助这个指针完成对内存的读写操作。需要使用GlobalUnlock函数来解除之前锁定的内存,该函数会让内存块的指针失效。接着调用GlobalFree函数来释放内存区域。传递给这个函数的是内存的标识码。

GlobalAlloc用于申请一块全局内存,其返回值是长整型数据,代表全局内存的句柄。如果操作未成功,返回值会是零。设置GetLastError参数表参数 类型及说明wFlags Long,该参数用于定义内存分配的类型,具体常数标志如下:GMEM_FIXED 指示分配固定内存块,GMEM_MOVEABLE 指示分配可移动内存块,GMEM_DISCARDABLE 指示分配可丢弃内存块,GMEM_NOCOMPACT 表示堆在函数调用期间不累积,GMEM_NODISCARD 表示函数调用期间不丢弃任何内存块,GMEM_ZEROINIT 表示新分配的内存块全部初始化为零 dwBytes Long,该参数表示需要分配的字符数量

GlobalLock函数的作用是固定一个全局内存区,返回该内存区首字节的地址,函数的原型为LPVOID GlobalLock( HGLOBAL 内存句柄 )

参数:hMem:全局内存对象的句柄。这个句柄是使用GlobalAlloc或GlobalReAlloc函数获取的输出结果:若调用成功,则输出结果为该对象首字节的地址,若调用失败,则输出结果为空指针,此时可通过GetLastError函数查询错误详情务必注意,一旦通过GlobalLock函数锁定某内存区域,就必须调用GlobalUnlock函数来解除锁定

GlobalUnlock函数作用说明:用于解锁已被锁定全局内存对象的函数,其函数声明为:BOOL GlobalUnlock( HGLOBAL hMem );参数hMem表示全局内存对象的句柄,返回值非零时,该内存对象依然处于锁定状态,返回0则表明函数调用失败,此时可借助GetLastError获取具体错误代码,若返回NO_ERROR则说明内存对象已成功解锁,特别提示:该函数实际操作是减少内存对象的锁定次数计数,若计数不为0,意味着此前多次使用GlobalLock对该内存对象进行过加锁操作,因此需要相应数量的GlobalUnlock函数来执行解锁。当GetLastError函数返回错误码为ERROR_NOT_LOCKED时,说明当前没有锁定状态,或者锁定已经被解除。

申请内存 hMem = 常规分配(GMEM_MOVEABLE | GMEM_DDESHARE, nSize);锁定内存 pMem = (BYTE *) 常规锁定(hMem);……

释放内存 GlobalUnlock(hMem),然后调用 GlobalFree 释放 hMem

三 总结

C/C++语言的一个显著特点就是它的灵活性,但这种特性也给C/C++程序员带来了挑战。随着程序规模的扩大,内存管理的工作量也随之增加,稍有不当就可能引发内存错误。内存泄漏是这类错误中的一种典型情况。即便内存泄漏问题并不算特别严重,在短时间内它对程序运行的影响也相对有限,这就使得这类问题具有很强的隐蔽性,不容易被人们察觉。即便内存泄漏看似微不足道,一旦程序持续运行,其造成的损害却十分巨大,可能表现为运行速度变慢,直至内存资源被完全耗尽,甚至波及其他程序的正常运作。内存故障通常具有一个显著特征,即故障本身不会产生直观的迹象,等到异常情况显现,问题发生的初始状态已经无法捕捉,这使得排查内存故障变得异常棘手。

安装Windows调试软件包完成后,借助其自带的gflags程序开启PageHeap功能,然后通过gflags -p /enable 程序主进程名 /full 参数设定,接着在IDE中以调试模式启动应用程序,迅速定位了问题所在,原因是某个静态方法里存在输入错误。在开发可靠的服务器端软件时,此类诊断工具具有显著价值。

首先,我们来看HeapAlloc,MSDN上的解释如下:HeapAlloc用于从堆中获取一块内存,且所获取的内存区域不可移动,若没有连续的足够空间,程序无法整合其他零散空间来满足需求,进而可能导致分配失败;这种分配方式从特定地址开始,与GlobalAlloc不同,GlobalAlloc是从全局堆中进行分配,其地址可能是全局的,也可能是局部的。函数的原型为:LPVOID HeapAlloc(HANDLE hHeap,DWORD dwFlags,SIZE_T dwBytes);其中,hHeap代表进程堆内存的起始位置。

分配堆内存时使用的标记是dwFlags,其中包含HEAP_ZERO_MEMORY,表示分配的空间会被初始化为零。需要分配的内存字节数由dwBytes指定。释放已分配的内存应调用HeapFree函数。

再看GlobalAlloc,这个函数用于从全局堆里分配内存供程序使用,它的函数原型是HGLOBAL GlobalAlloc(UINT uFlags,SIZE_T dwBytes);其中uFlags参数包含GHND GMEM_MOVEABLE和GMEM_ZEROINIT的组合,如果选择GMEM_FIXED,会分配固定内存,返回值是一个指针,如果选择GMEM_MOVEABLE,会分配活动内存,在Win32系统里,内存块不能在物理内存中移动,但可以在默认的堆里移动。返回值代表内存对象的引用,借助函数GlobalLock,该引用能转变为指针,GMEM_ZEROINIT用于将内存数据清零,GPTR与GMEM_FIXED结合使用,通常在编写程序时,为软件分配的内存允许移动或释放,这有助于高效利用内存,因此,有时分配的内存地址会变动,因为它是可移动的,所以必须先固定内存区域,为此,软件需要调用API函数GlobalLock来锁定引用。内存区域需要先通过GlobalLock函数锁定,然后程序才能访问该内存,因此在使用GlobalLock时,一般会配合GlobalUnlock使用,当内存不再需要时,一定要调用GlobalUnlock,否则被锁定的内存空间将无法被其他程序或变量所利用。

分配给全局内存的内存块,应当通过全局释放函数来回收。

LocalAlloc这个函数是用来从局部堆里分配内存空间给程序用的,它的函数原型是HLOCAL LocalAlloc(UINT uFlags,SIZE_T uBytes);参数和GlobalAlloc是一样的。在16位Windows系统中,这个函数和GlobalAlloc是有区别的,因为在16位Windows里,内存管理使用的是全局堆和局部堆两种方式。每个应用程序或dll在装入内存时,代码部分会被放到全局堆里,而系统会从全局堆中为每个实例单独分出64KB的数据段作为局部堆,这个局部堆专门用来存放应用程序的堆栈以及所有全局或静态变量。LocalAlloc和GlobalAlloc是两个不同的函数,它们分别用于在局部内存区域和全局内存区域中申请内存空间。

每个进程的私有堆空间有限,因此从该堆分配内存容易遇到容量不足的问题。不过这种堆是进程专属的,因而分配数据时更为稳妥,一旦访问出现异常也不会波及整个系统。与此不同,全局堆供所有进程共同使用,任何进程只要持有该内存区域的引用就能进行访问。然而这种共享机制需要额外预留内存空间,从而引发资源分配效率低下的问题。而且一旦发生严重错误,可能会影响到整个系统的稳定。

在Win32环境下,每个进程仅配备一个固有的私有内存区,该区域仅限于本进程使用。程序无法直接接触系统内存。因此全局内存区和局部内存区都映射到进程的固有内存区。采用LocalAlloc/GlobalAlloc分配内存效果相同。即便LocalAlloc分配的内存也能被GlobalFree回收。因此,在Win32平台进行开发,不必关注静态存储和动态存储的差异,常规的内存申请操作均等同于调用HeapAlloc配合GetProcessHeap函数完成。

LocalAlloc对应的释放函数为LockFree.

VirtualAlloc,这个函数的作用是在当前运行程序的内存地址范围内,申请或者使用一部分内存页面,如果这次内存申请不要求系统重置内容,并且没有特别指明分配方式,那么系统会自动把这块内存区域清零;它的使用格式是这样的:LPVOID VirtualAlloc(LPVOID lpAddress, // 指定内存区域起始地址 SIZE_T dwSize, // 需要的内存大小 DWORD flAllocationType, // 分配方式 DWORD flProtect // 内存访问权限);这个函数支持分批多次申请,以此来逐步完成一个较大内存区域的预留。反复向同一处区域提交不会导致错误。这让一个应用程序在保留内存后能够自由提交即将写入的页。如果这种方式失效了,它会释放应用程序,通过检查被保留页的状态,看它是否在提交调用前已被提交。

VirtualAlloc的对应释放操作是VirtualFree.

malloc属于C++/C语言的标准库函数,能够分配动态内存空间,同时也能释放内存空间,但它们无法应对非内部数据类型对象的动态管理需求,因为对象在生成时必须自动调用构造函数,在销毁前必须自动调用析构函数malloc和free属于库函数范畴而非运算符,它们不受编译器管理,因此无法将构造函数和析构函数的调用义务施加于它们。

C++中有运算符new和delete,这两个运算符可以用来申请动态内存以及释放内存,new运算符能够完成动态内存分配和初始化工作,而delete运算符则能执行清理与释放内存的任务,注意这两个运算符并不是库函数C语言程序常常需要借助C函数,而C语言只能通过malloc和free来处理动态内存分配。new是一个运算符,其地位与“+”“-”“=”等运算符相同。

malloc是个分配内存的函数,供你调用的。

new是保留字,不需要头文件支持。

malloc函数调用需要引入对应的头文件和库函数。new操作会生成一个对象实例,而malloc函数只是开辟了一片内存区域。

新建的实例可以当作常规实例看待,通过方法调用进行操作,不宜直接探查其内存地址,动态内存则应由指针来引用,并且允许指针在区域内游走

内存泄漏情况在malloc或者new时都能检测到,两者不同之处在于new能够标示出具体是哪个文件以及哪一行产生的,而malloc则不具备这些功能。new可以看作是malloc执行的基础上增加了构造函数的调用。new创建的指针直接带有类型信息,而malloc返回的都是void类型的指针。

相关推荐: