0 堆内存的在计算机内存中的形式

根据《The C Programming language》推测得到堆内存,图中的Heap区域即为堆内存块(Heap区域的数目不代表计算机堆内存的真实数目)。

[1] 堆内存不连续。只有标识为Heap的才是堆内存。

[2]  在malloc()/free()看来,每个Heap所代表的的堆由两部分组成:Header +可给用户使用的堆内存。在Header中包含了“指向下一邻近高地址堆内存块的指针”、“本堆块的大小”。每次由malloc()函数分配给用户的堆内存也必须包含Header结构(且所占内存就在返回给用户使用的堆内存之前),这样是为了让malloc()/free()更好的管理堆内存。

[3] malloc()/free()函数操作的堆内存是如图所示的一个链(Heap1 -> Heap2 ->Heap3 ->Heap4 ->Heap1),可通过此链表访问到任意一段堆内存。所以,经malloc()函数实际分配得到的堆内存要比用户实际需求的要大一个Header,只是返回给用户的堆内存大小刚好是用户所需。free()释放时,也要根据Header的内容将此段曾供给用户使用过得堆内存释放到最邻近的一个堆块中去。

这就是内存中的堆内存。堆内存由用户用代码分配及回收。堆和栈的区别不仅在于内存的存在形式,在使用时栈一般拥有内存名即栈内存可以由内存名(变量名)直接访问,也可以通过地址(指针)访问栈内存。但对于堆内存来说,堆不存在内存名,只有通过地址(指针)访问。

1堆内存

Figure1:内存中的堆内存空间

假设从《The  C  Programming  Language》中推测正确,从未经动态分配的堆内存呈现上图形式。不连续的堆内存以“链”的形式联系:Heap1 -> Heap2 ->Heap3 ->Heap4->Heap1。笔迹将构成“堆链”的每个堆内存(如Heap1)称为“堆块”。malloc()/free()将每个堆块看作由两部分构成:“Header”和“可用堆内存”。在Header中包含了“指向下一个堆内存块的指针”、“本堆块的大小”。这样malloc()/free()就能更好地管理堆。

2 堆内存分配

[1] mallco()分配机制

根据C中malloc(n)函数动态分配堆的机制:分配堆内存的时候就依序由低到高的地址搜索“堆链”中的堆块,搜索到“可用堆内存”满足n的堆块(如Heap1)为止。若Heap1的“可用堆内存”刚好满足n,则将Heap1从“堆链”中删除,同时重新组织各堆块形成新的“堆链”;若Heap1的“可用堆内存”大小大于n,则将malloc(n)申请到的“Header” + "可用堆内存"部分从Heap1中分裂,将剩余的Heap1堆内存块重新加入“堆链”中。经分裂后的堆内存也包含“Header”和“可用堆内存”两部分(如图Figure 2),然后将由malloc()分配得到的“可用堆内存”返回给用户。若某块堆内存空间比较大(如Heap1),能够满足较小内存的多次申请,那么由malloc(n)多次申请的堆内存块都是连续被分配给用户的(因为有Header,所以用户使用的堆地址不连续)

 
为方便free()释放堆空间,经malloc(n)分配给用户的堆空间也隐含一个Header。如下图所示:
Figure2:malloc()分配的堆内存结构

由于Header的构成的内存对齐,C中malloc(n)函数分配的堆内存会大于等于Header + n。

3 malloc()分配内存

可先参见位经malloc()函数申请分配的堆内存在计算机中的形式:计算机中的堆

经malloc()分配过得堆内存结构如下:

Read From《The C Programming Language》。

可用的堆内存块以“可用堆内存链表”的形式存在。malloc()进行动态分配的特点:

  • malloc()根据用户所需分配内存的大小n (bytes)在“堆链表”(见未使用过得堆内存)里搜索。直到搜索到一个大于等于n字节的堆内存块为止。如果此堆内存块的大小刚好为n,则直接将首地址返回给用户;如果此内存块的大小大于n,则将此块堆内存分裂,将大于n部分的堆内存留在可用堆内存中,以“堆链表”的形式和其它未分配的堆内存发生联系。
  • 如果整个堆链表所代表的堆内存块都没有大于等于n的堆内存块,系统将给“堆链表”链接一个更大的区域供其使用。要是这一步也失败了,malloc()函数就返回NULL给用户。

malloc()函数分配内存成功则返回可用堆内存块的首地址,若分配失败则返回空。在使用malloc()后一定要判断堆内存是否成功。若对内存分配未成功使用指针操作内存也会使程序出现异常。动态分配内存时要采取以下结构:

  1. char *pL =NULL;
  2. ……
  3. pL       = (char *)malloc( sizeof(char) * size);
  4. if(pL)
  5. {
  6. }

分配成功后,得到的堆内存首地址一定要保存,不然后来无法释放堆内存而造成内存泄露。而且不可使用未初始化的pL指向的内存块。

4 用指针来使用堆空间

  • 定义指针后,释放堆空间后都应将指针赋值为NULL。若指针之上有地址值,而以此地址值为起始地址的内存空间不再可用,则就形成了野指针,野指针有潜在的危险。
  • 在上一点的基础之上,使用指针前判断其值是否为NULL。
  • 以指针为索引(堆内存无名),若malloc分配内存成功,初始化堆内存(malloc时,大小要不为0)。malloc前的强制转换类型规定了申请的堆内存将要存的数据类型。
  • free堆内存后,指针保存的地址值还在,只是那块内存已经被回收了,所以需要再次将指针的值设为NULL,避免使用野指针。free内存时,按照逻辑来,防止内存泄露。

指针名所代表的4 bytes内存上存了堆内存的首地址后,访问这块堆内存内容跟平时使用指针差不多。可以以指针的形式访问(甚用p++ || ++p,堆内存首地址可不要丢失,留着释放),也可以使用下标的形式访问。

5 free()内存

当使用free()函数释放堆内存的时候,free()函数将堆内存插入到于要释放堆内存地址最邻近的一个位置上,尽可能的使堆内存以大块的形式存在而不至于让堆内存称为碎片。

释放未指向任何堆内存块的指针也会造成内存泄露。所以在释放指针前的一个基本操作是判断指针内容是否为空,free(p)后只是将p指向的内存回收,p的值依旧存在,为避免再次使用p的值还需要将p赋值为NULL(因为使用指针前都会判断是否为NULL)。释放堆内存块采取这样的程序结构:

  1. if(pL)
  2. {
  3. free(pL);
  4. pL      = NULL;
  5. }

6 指针赋值为NULL的道理

有笔记“C中的void和NULL”表面引用NULL指针的后果。为了更好的利用指针,避免野指针(指针所指的内存块不可用)的使用在所有使用指向堆内存块的指针前都采取如此的结构:

  1. if(p)
  2. {
  3. //通过指针操作堆内存
  4. ……
  5. }

定义指针后将其值赋值为NULL。此时指针指向的内存地址为NULL,NULL对指针的赋值是将指针置成空指针(什么也没有指向)还是将指针指向了一段特殊的地址取决于编译器,编程中我们不需要了解NULL到底代表什么,只需要用NULL来避免指针带来的后果。

定义指针后将其赋值为NULL之后的好处在于避免系统给予局部指针变量的随机值,我们在使用指针前(malloc()除外)都判断一下指针的值是否为NULL,只有在不为空的情况下才能对此进行操作,如free(p),若在不判断p是否为空的情况下进行free(p)操作则会造成内存泄露。

7 在含指针参数的函数内使用断言

(1)用断言判断指针是否为NULL

判断指针是否为NULL的主要针对对象是指向堆内存的指针。比如在以下内存拷贝函数中:

  1. flag  my_strcpy(char  *StrTo,  char  *StrFrom)
  2. {
  3. if(!StrTo || !StrFrom)
  4. {
  5. return  -1;
  6. }
  7. char  *StrToL, *StrFromL;
  8. StrToL      = StrTo;
  9. StrFromL    = StrFrom;
  10. while(*StrToL++ = * StrFromL++)
  11. NULL;
  12. return 0;
  13. }

程序中首先判断两个地址是否为空。判断StrTo是为了了解StrTo是否指向一段空间。当然若StrTo指向一个常数,往后拷贝操作还得出错。

像这样带指针参数的子函数内都很有必要有这么一段判断指针是否为NULL的语句,故而可以将这样的代码写成函数来供大家使用,再考虑此代码段比较小可以用宏代替。这样的(带参数)宏可称为断言,因为当指针未空时就退出子函数(如assert())。

如以上一段判断子函数是否为空可以用如下宏代替,形成一个断言:

  1. #define  MY_ASSERT(pStrTo, pStrFrom)     if(!StrTo || !StrFrom)     \
  2. {                          \
  3. return  -1;           \
  4. }

然后在每个程序中直接调用MY_ASSERT(pStrTo, pStrFrom);即可。由于这样的宏(断言)可能供许多函数的使用,所以一定要保证它的正确性。

(2)内存块重叠

内存块重叠指多个指针指向的内存有重叠的情况。对内存块的操作是否会影响源内存块的内容(如内存数据拷贝)。

两指针指向的内存块重叠

如上图将p2指向内存的数据拷贝给p1代表的内存中去后,p2指向的内存块数据也被改变。堆内存块的操作不要有副作用。

8 总结

(1)用NULL(因其特殊性)来统一标识指针的可用性。使用指针前都应该判断一下指针是否为NULL。

(2)将局部指针变量初始化为NULL(消除系统给其赋的随机值,系统为其赋随机值也就造就了野指针)。

(3)指针用于指向堆内存时需要注意:

  • malloc()后一定要判断是否malloc()成功。malloc()成功后一定要保存所分配堆内存块的首地址。

  • 使用堆内存块前要初始化。

  • 使用堆内存块不可越界。

  • 正确释放每个堆内存块。且释放后将指针的值重新赋值为NULL。

(4)使用指针前都应该判断一下指针是否为NULL。

完全使用完某个指针或释放指向堆内存的指针后,将其值赋值为空。指向堆内存的指针在释放完需要赋值为空的理由见free ()堆内存。对于指针定义时初始化和完全使用完指针后再将其值赋为NULL的道理在于所有使用指针的语句前都会有有判断指针是否为NULL的语句。尤其是在子函数内判断指向堆内存块的指针实参是否为NULL

9 malloc()分配总结

对于C中的malloc(n)分配,有以下进一步的结论:

  • 实际分配的堆内存是Header + n结构。返回给用户的是n部分的首地址。
  • 由于内存对齐值8,实际分配的堆内存大于等于sizeof(Header) + n。
所以在编程过程中,需要用内存对齐的知识合理的让malloc()分配的内存变得小一些。

malloc的内存分配原理的更多相关文章

  1. 深入理解golang:内存分配原理

    一.Linux系统内存 在说明golang内存分配之前,先了解下Linux系统内存相关的基础知识,有助于理解golang内存分配原理. 1.1 虚拟内存技术 在早期内存管理中,如果程序太大,超过了空闲 ...

  2. 深入Java核心 Java内存分配原理精讲

    深入Java核心 Java内存分配原理精讲 栈.堆.常量池虽同属Java内存分配时操作的区域,但其适用范围和功用却大不相同.本文将深入Java核心,详细讲解Java内存分配方面的知识. Java内存分 ...

  3. JVM内存分配原理

    堆栈常量池等内存分配原理详解 存储的方式: 寄存器 栈(stack) 堆(heap) 静态域 常量池 非RAM存储 JAVA寄存器 最快的存储区, 由编译器根据需求进行分配,我们在程序中无法控制.  ...

  4. 【转】linux环境内存分配原理 malloc info

    Linux的虚拟内存管理有几个关键概念: Linux 虚拟地址空间如何分布?malloc和free是如何分配和释放内存?如何查看堆内内存的碎片情况?既然堆内内存brk和sbrk不能直接释放,为什么不全 ...

  5. linux环境内存分配原理 mallocinfo

    Linux的虚拟内存管理有几个关键概念: Linux 虚拟地址空间如何分布?malloc和free是如何分配和释放内存?如何查看堆内内存的碎片情况?既然堆内内存brk和sbrk不能直接释放,为什么不全 ...

  6. linux环境内存分配原理 mallocinfo【转】

    转自:http://www.cnblogs.com/dongzhiquan/p/5621906.html Linux的虚拟内存管理有几个关键概念: Linux 虚拟地址空间如何分布?malloc和fr ...

  7. TCMalloc 内存分配原理简析

    一.TCMalloc TCMalloc简介 为啥要介绍 TCMalloc? 因为golang的内存分配算法绝大部分都是来自 TCMalloc,golang只改动了其中的一小部分.所以要理解golang ...

  8. java内存分配原理

    一般Java在内存分配时会涉及到以下区域: ◆寄存器:我们在程序中无法控制 ◆栈:存放基本类型的数据和对象的引用,但对象本身不存放在栈中,而是存放在堆中 ◆堆:存放用new产生的数据 ◆静态域:存放在 ...

  9. JAVA中堆栈和内存分配原理

    1.栈.堆 1.寄存器:最快的存储区, 由编译器根据需求进行分配,我们在程序中无法控制.2. 栈:存放基本类型的变量数据和对象的引用,但对象本身不存放在栈中,而是存放在堆(new 出来的对象)或者常量 ...

随机推荐

  1. 11个JavaScript颜色选择器插件

    几年前,很难找到一个合适的颜色选择器.正好看到很多不错的JavaScript颜色选择器插件,故而把这些编译汇总.在本文,Web设计师和开发人员 Kevin Liew 选取了11个相应插件,有些会比较复 ...

  2. Android程序apk反编译破解方法

    简短不割了,我们直接奔主题吧. 把apktool-install-windows-r05-ibot文件里的两个文件剪切到apktool1.5.1目录. 新建一个文件夹把需要破解的apk应用程序放进去. ...

  3. Global and Local Coordinate Systems

    ansys 中的坐标系 整体和局部坐标系(主要在建模中涉及) 整体坐标系是以你建模的整个建筑为一体,来确定坐标系的.比如你建一个矩形平面的建筑,整体坐标系一般默认水平方向为X轴,竖直方向为Y轴,以垂直 ...

  4. 微信小程序价值思考:手机端的CS-BS迁移

    从很多特点来看,小程序都非常类似于网页:主要的业务逻辑在服务端.客户端无需安装应用程序.小程序的开发采用的HTML+JS+CSS技术等等.张小龙自己对小程序的定位也大概如此:无意做小程序分发平台,只是 ...

  5. [Javascript] Using map() function instead of for loop

    As an example, if Jason was riding the roller coaster (and when isn’t he), your goal would be to cha ...

  6. ListView与Button共存问题

        转载:http://blog.csdn.net/xinqiqi123/article/details/6458030 ListView 和 其它能触发点击事件的widget无法一起正常工作的原 ...

  7. 能说明你的Javascript技术很烂的五个原因

    Javascript在互联网上名声很臭,但你又很难再找到一个像它这样如此动态.如此被广泛使用.如此根植于我们的生活中的另外一种语言.它的低学习门槛让很多人都称它为学前脚本语言,它另外一个让人嘲笑的东西 ...

  8. GCD多线程使用

    - (void)showTaped { /* dispatch_get_global_queue dispatch_get_main_queue dispatch_queue_create dispa ...

  9. PHP高级教程-包含

    PHP 包含文件 PHP include 和 require 语句 在 PHP 中,您可以在服务器执行 PHP 文件之前在该文件中插入一个文件的内容. include 和 require 语句用于在执 ...

  10. 学习中遇到的c++问题,持续更新

    原文请訪问我的博客:http://xiaoshig.sinaapp.com/ 向上取整 使用ceil函数.ceil(x)返回的是大于x的最小整数.如: ceil(2.5) = 3 ceil(-2.5) ...