简介
什么是代码区、常量区、全局/静态区、堆区、栈区? 每次听到这些区头都大了,很混乱而且经常忘记。其实C语言程序常涉及的基本就5个内存区:
- 栈区主要用于函数调用的使用
- 堆区主要是用于内存的动态申请和归还
- 全局/静态存储区用于保存全局变量和静态变量
- 常量区存放的是常量,不允许修改,程序结束后由系统释放。
- 代码区用于存放函数体的二进制代码,也是由系统管理。
下面来重点温习一下栈,堆和静态区:
栈区
栈是现代计算机程序里最为重要的概念之一。
栈在程序中用于维护函数调用上下文,没有栈就没有函数,没有局部变量。栈保存了一个函数调用所需的维护信息:
- 函数参数,函数返回地址
- 局部变量
- 函数调用上下文
栈上的数据在函数调用时申请在函数返回后就会被释放掉,无法传递到函数外部,如:局部数组,局部变量等。
栈内存是由编译器自动分配与释放的,它有两种分配方式:静态分配和动态分配。
- 静态分配是由编译器自动完成的,如局部变量的分配(即在一个函数中声明一个int类型的变量i时,编译器就会自动开辟一块内存以存放变量i)。与此同时,其生存周期也只在函数的运行过程中,在运行后就释放,并不可以再次访问。
- 动态分配由 alloca 函数进行分配,但是栈的动态分配与堆是不同的,它的动态分配是由编译器进行释放,无需任何手工实现。值得注意的是,虽然用 alloca 函数可以实现栈内存的动态分配,但 alloca 函数的可移植性很差,而且在没有传统堆栈的机器上很难实现。因此,不宜使用于广泛移植的程序中。当然,完全可以使用C99中的变长数组来替代 alloca 函数(在C99标准之前,C语言是不支持变长数组的,如果想要动态开辟栈内存以达到变长数组的功能就得依靠 alloca 函数)。
- 另外栈内存由一个栈指针来开辟和回收,栈内存是从高地址向低地址增长的,增长时,栈指针向低地址方向移动,指针的地址值也就相应的减小;回收时,栈指针向高地址方向移动,地址值也就增加。所以栈内存的开辟和回收都只是指针的加减,由此相对于分配堆内存可以获得一定的性能提升。
堆区
堆是程序中一块巨大的内存空间,堆内完全是由程序员手动申请与释放的,程序在运行的时候由程序员使用内存分配函数(如 malloc 函数)来申请任意多少的内存,使用完再由程序员自己负责使用内存释放函数(如 free 函数)释放内存,堆中被程序申请使用的内存在程序主动释放前将一直有效。
但是对堆来说,频繁分配和释放(malloc/free)不同大小的堆空间势必会造成内存空间的不连续,从而造成大量碎片,导致程序效率降低;而对栈来讲,则不会存在这个问题。
静态区
又叫全局区,分为全局初始化区(data segment)和未初始化区(bss segment)。全局的未初始化变量存在于.bss段中,具体体现为一个占位符;全局的已初始化变量存于.data段中。.bss是不占用可执行文件空间的(并不给该段的数据分配空间,只是记录数据所需空间的大小),其内容由操作系统初始化(清零);而.data却需要占用,其内容由程序初始化。
静态区是由编译器自动分配和释放的,即内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在,直到整个程序运行结束时才被释放,如全局变量与static变量。
与栈和堆不同,静态存储区的信息最终会保存到可执行程序中。
效率
大家都知道,栈是机器系统提供的数据结构,计算机会在底层对栈提供支持,例如,分配专门的寄存器存放栈的地址,压栈出栈都有专门的执行指令,这就决定了栈的效率比较高。一般而言,只要栈的剩余空间大于所申请空间,系统就将为程序提供内存,否则将报异常提示栈溢出。
而堆则不同,它是由 C/C++ 函数库提供的,它的机制也相当复杂。例如,为了分配一块堆内存,首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆节点,然后将该节点从空闲节点链表中删除,并将该节点的空间分配给程序。而对于大多数系统,会在这块内存空间的首地址处记录本次分配的大小,这样,代码中的 delete 语句才能正确释放本内存空间。另外,由于找到的堆节点的大小不一定正好等于申请的大小,系统会自动将多余的那部分重新放入空闲链表中。很显然,堆的分配效率比栈要低得多。
区的大小
由于操作系统是用链表来存储空闲内存地址(内存区域不连续)的,同时链表的遍历方向是由低地址向高地址进行的。因此,堆内存的申请大小受限于计算机系统中有效的虚拟内存。
而栈则不同,它是一块连续的内存区域,其地址的增长方向是向下进行的,向内存地址减小的方向增长。由此可见,栈顶的地址和栈的最大容量一般都是由系统预先规定好的,如果申请的空间超过栈的剩余空间时,将会提示溢出错误。由此可见,相对于堆,能够从栈中获得的空间相对较小。