内存分配的概念

      我们写过很多c/c++代码(或者其他编程语言),然后通过编译器进行编译再运行一个程序,要使用数据对象(例如变量、类对象)是得要分配内存的,而大家可能不太熟悉这些数据对象是如何分配的。那么接下来,笔者为大家逐步分析并解决这些疑惑。

      我们写完代码,通过编译器进行编译之后,C/c++程序便生成了二进制映像文件(也称为目标文件(c++里面是.obj),是按照可执行文件(c++中的.exe)格式存储的,但不是真正意义上的可执行文件,目标文件只是还没有通过链接装载的过程),这文件的组成成分包含:栈区,堆区,数据段已经初始化读写数据段、只读数据段、BSS段(未初始化数据段))和代码段。(程序没有运行的时候,这二进制映像文件暂时存放在硬盘当中的;当程序被打开的时候,根据文件的需要,每个区域映射到系统分配相应的内存当中)

二进制映像文件分配区域和程序运行时分配内存的图解:

 

在操作系统中,一个进程就是处于执行期的程序(当然包括系统资源),实际上是正在执行的程序代码的活标本。那么进程的逻辑地址空间是如何划分的呢?

以下是程序运行时,二进制映像文件映射到内存的图解:

此图出自某网友的一篇文章

(左边的是UNIX/LINUX系统的执行文件,右边是对应进程逻辑地址空间的划分情况。)

动态存储方式

意义:所谓动态存储方式是指在程序运行期间据需要进行动态的分配存储空间的方式。
动态存储变量是在程序执行过程中,使用它时才分配存储单元, 使用完毕立即释放。
 

典型的例子是函数的形式参数,在函数定义时并不给形参分配存储单元,只是在函数被调用时,才予以分配,调用函数完毕立即释放。如果一个函数被多次调用,则反复地分配、释放形参变量的存储单元。

1.栈区(stack):

==程序运行时,负责栈区的操作者:操作系统

由系统自动分配释放

==存储的数据:

1.函数的形参变量

2.自动变量(auto register):函数的普通局部变量,不包括静态局部变量

3.函数调用现场保护和返回地址等。

==特征:其操作方式类似于数据结构中的栈

==组成部分:

在栈区里面其实又可以分成好几个区域,他们叫做栈桢,一个栈桢就是一个函数,需要调用该函数的时候就如入栈,函数return的时候就会弹出栈,所以他们的生命周期是从函数的开始直到函数结束。
而栈帧里面又存放着什么呢,栈帧存放着以下几种东西:参数变量的地址,局部变量的地址,return的地址、栈指针和基指针等。

==产生栈区的目的:作为暂时存储器,因为频繁调用一些函数,存储的变量会频繁分配又释放,不会长期占有内存。

==作用域:函数体内部

==生命周期:限制在函数执行时间内

2.堆区(heap):

==程序运行时,负责堆区的操作者:操作系统或者程序员。

操作系统:程序结束时可能由OS(操作系统)回收。

程序员:堆允许程序在运行时动态地申请某个大小的内存。

一般由程序员分配释放,若程序员不释放,则可能会引起内存泄漏,或者==通过 new 算符和 malloc 函数分配得到的空间

==特征:堆和数据结构中的堆栈不一样,这里的堆的分配方式类似于链表。

== =作用域:由引用该内存的指针决定

==生命周期:直到内存释放(程序员手动释放或者程序结束时由操作系统回收)

静态存储方式

意义:所谓静态存储方式是指在程序编译期间分配固定的存储空间的方式,不需要观察程序运行做了什么。

该存储方式通常是在变量定义时就分定存储单元并一直保持不变, 直至整个程序结束。全局变量(包括静态全局变量和普通全局变量),静态变量 等就属于此类存储方式。 

以下是笔者画的静态存储区域分类图:   

3.代码段(Text)

= =存放函数体的二进制代码。

==程序指令

(程序运行的时候,把代码中的指令<例如两个变量ab进行加法运算“a+b”的指令>拷贝到相应的内存中等待cpu执行该指令)

4.数据段,有三部分组成

(注意:以下的静态变量区和上面讲的静态区域(也叫静态存储区)是不一样的概念。)

<4.1>只读数据段(Data)(包含有常量区)
只读数据区是程序使用的一些不会被更改的数据,使用这些数据的方式类似查表式的操作,由于这些变量不需要更改,因此只需要放置在只读存储器中即可。一般是const修饰的变量(部分const 修饰的变量不属于只读数据段)以及程序中使用的文字常量一般会存放在只读数据区中。

==特点:只读不可写性

<附加>常量区(特殊的常量存储区,属于静态存储区,在只读数据段,一些常量也有存在于代码段(由于和代码段混合在一块,这些常量不存在于常量区))
  1) 常量占用内存,只读状态,决不可修改

(不过有些特殊情况,有些常量是不占有内存的。比如不同编译器的优化情况,后面(1.8常量和const的讨论)会讲到)
  2) 常量字符串就是放在这里的,程序结束后由系统释放

(char *c="123456"中的“123456”就存放在文字常量区,还有int c=12中的“12”这个是字面值常量 因为不是在只读数据段的常量区,所以存放在代码段)
<4.2>已初始化的读写数据段 (含有 全局变量区和静态变量区混合成一块区),也就只有这一块区域):

已初始化数据是在程序中声明,并且具有初值的变量,这些变量需要占用存储器的空间,在程序执行时它们需要位于可读写的内存区域内,并且有初值,以供程序运行时读写。在程序中一般为已经初始化的全局变量已经初始化的静态局部变量(这里注意是静态局部变量,也就是在函数体内部引用之后,直到程序结束才释放该内存,和栈区没有关系,是static修饰的已经初始化的变量)

==特点:具有可读写性。

<4.3>未初始化段(BSS)(含有全局变量区和静态变量区,是两块彼此相邻的区
==BSS段通常是指用来存放程序中没有初始化的全局变量(全局变量区)没有初始化的局部静态变量(静态变量区)的一块内存区域。

==是否占用空间的情况:

       可执行文件必须记录未初始化全局变量和局部静态变量的大小总和,只是为这些变量预留位置而已(因为这个原因才设定BSS段);BSS段并没有内容,只是记录一些信息(未初始化数据在程序中声明,指定某种数据类型以及大小等信息),所以这些变量在程序运行之前是不需要占用目标文件的空间(也可以说存储器的空间,硬盘)的,但是程序运行时是占有内存的(系统会根据BSS段记录的信息,然后将BSS的变量分配相应的内存大小,此时BSS段的数据在系统中才占有内存,不过此时在目标文件当中仍然没有占用空间)。

== 产生BSS段的目的:节省磁盘空间

==特点:

具有可读写性,在程序执行之前BSS段会自动清0。对象变量的值自动清NULL。

即使全局、静态数据变量初始化为0仍然是属于BSS段。

附加说明:

<附加知识点>

**全局变量区

作用域:整个文件

生命周期:应用程序的生命周期(程序运行到结束,系统才释放这些分配好的内存)

**静态变量区:

作用域:静态局部变量在声明它的函数体内部才被引用,静态全局变量在整个文件都有作用。

生命周期:无论是全局静态变量还是静态局部变量,都是应用程序的生命周期

 

---------------------------------------------------


动态存储区和静态存储区的区别   

总结
从以上分析可知, 静态存储区域的变量是一直存在的, 而动态存储变量则时而存在时而消失。我们又把这种由于变量存储方式不同而产生的特性称变量的生存期。 

生存期表示了变量存在的时间。 生存期和作用域是从时间和空间这两个不同的角度来描述变量的特性,这两者既有联系,又有区别。 一个变量究竟属于哪一种存储方式, 并不能仅从其作用域来判断,还应有明确的存储类型说明。

二.堆和栈的区别

相同点

堆和栈在内存中分配位置,跟硬件架构和操作系统都有关系。

不同点:

1.申请方式

(1)栈(satck):由系统自动分配并释放。


  1)程序运行时由编译器自动分配的一块连续的内容,存放函数的参数值,局部变量的值等。
例如,声明在函数中一个局部变量int b;系统自动在栈中为b开辟空间。
  2)程序结束时由编译器自动释放

  3) 栈由系统自动分配,程序员无法控制

  4)只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
  5)存取方式,先进后出

(2)堆(heap):由程序员分配释放

      1)在内存开辟另一块不连续的存储区域。

  2)若程序员不释放,程序结束时由系统回收。
  3)首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。

       需程序员自己申请(调用malloc,realloc,calloc),并指明大小,并由程序员进行释放。容易产生memory leak.eg:(内存泄漏)

位置分配不同的区别

==x86中栈都是由高地址向低地址分配,堆是由低地址向高地址分配。由此可知,在 Windows 和 Linux 中堆和栈的位置相反,另外存放静态数据、代码的区域位置也有一些不同。

==栈是属于线程的,每一个线程会有一个自己的栈。

2.申请大小的限制

        (1)栈:在windows下栈是向底地址扩展的数据结构,是一块连续的内存区域(它的生长方向与内存的生长方向相反)。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在 WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数)。

栈的大小是固定的。如果申请的空间超过栈的剩余空间时,将提示overflow(内存溢出)。因此,能从栈获得的空间较小。

         (2)堆:堆是向高地址扩展的数据结构(它的生长方向与内存的生长方向相同),是不连续的内存区域。这是由于系统使用链表来存储空闲内存地址的,自然是不连续的,而链表的遍历方向是由底地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
3.系统响应:
          (1)栈:只要栈的空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
         (2)堆:首先应该知道操作系统有一个记录空闲内存地址的链表,但系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的free语句才能正确的释放本内存空间。另外,找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

           说明:

(1)对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。

(2)对于栈来讲,则不会存在这个问题

4.申请效率的比较
      (1)栈由系统自动分配,速度快 。但程序员是无法控制的

      (2)堆是由malloc分配的内存,一般速度比较慢,而且容易产生碎片 ,不过用起来最方便。

===另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈,是直接在进程的地址空间中保留一块内存,虽然用起来最不方便。但是速度快,也最灵活。

5.堆和栈中的存储内容
        (1)栈:在函数调用时,第一个进栈的主函数中后的下一条语句的地址,然后是函数的各个参数,参数是从右往左入栈的(往后关于栈区的文章笔者会讲到),然后是函数中的局部变量。注:静态变量是不入栈的。当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续执行。
         (2)堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。

6.存取效率的比较

        (1)堆:char *s1=”hellow tigerjibo”;hellow tigerjibo是在编译是就确定的。

        (2)栈:char s1[]=”hellow tigerjibo”;hellow tigerjibo是在运行时赋值的;

用数组比用指针速度更快一些,指针在底层汇编中需要用edx寄存器中转一下,而数组在栈上读取。

栈在读取时直接就把字符串中的元素读到寄存器cl中,而堆则要先把指针值读到edx中,在根据edx读取字符,显然慢了

补充:

       栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。

       堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。

。(以上第5、6点,往后笔者会详细分析

7.分配方式:
(1)堆都是动态分配的,没有静态分配的堆。

(2)栈有两种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的。它的动态分配是由编译器进行释放,无需手工实现。

堆和栈的区别可以用如下的比喻来看出: 
使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。 

使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。

                                   ----出自百度百科“堆内存”的典型比喻

堆和栈的区别的总结: 
操作系统方面的堆和栈,如上面说的那些,不多说了。 
还有就是数据结构方面的堆和栈,这些都是不同的概念。这里的堆实际上指的就是(满足堆性质的)优先队列的一种数据结构,第1个元素有最高的优先权;栈实际上就是满足先进后出的性质的数学或数据结构。至于堆栈,堆栈的说法是连起来叫的,但是他们还是有很大区别的,连着叫只是由于历史的原因。
 (一些老版本的专业书籍所说的堆栈,实际上就是栈)

附带编译器把代码编译产生目标文件图:

这张图我搞错了,是摘自于《c专家编程》*_*!


猜你喜欢

转载自blog.csdn.net/chen1083376511/article/details/79482011