如何理解栈与堆?

Posted by     "hehaoran" on Thursday, December 10, 2020

TOC

1. 引言

堆(Heap)与栈(Stack)一般情况下, 有两层含义:

  • 程序内存布局场景下, 堆与栈表示的是两种内存管理方式;
  • 数据结构场景下, 堆与栈表示两种常用的数据结构。

2. 内存分区中的堆与栈

2.1 栈

栈由操作系统自动分配释放, 用于存放函数的参数值、局部变量等, 其操作方式类似于数据结构中的栈.

int main(void)
{
    int n = 1; // 栈
    char ch = 'a'; // 栈
    char str = "abc"; // 栈
    float pi = 3.14; // 栈
}

其中函数中定义的局部变量按照先后定义的顺序依次压入栈中, 也就是说相邻变量的地址之间不会存在其它变量. 栈的内存地址生长方向与堆相反, 由高到底, 所以后定义的变量地址低于先定义的变量, 比如上面代码中变量ch的地址小于变量n的地址. 栈中存储的数据的生命周期随着函数的执行完成而结束.

2.2 堆

堆是一种常用的树形结构, 是一种特殊的完全二叉树, 当且仅当满足所有节点的值总是不大于或不小于其父节点的值的完全二叉树被称之为堆. 堆的这一特性称之为堆序性. 因此, 在一个堆中, 根节点是最大(或最小)节点. 如果根节点最小, 称之为小顶堆(或小根堆), 如果根节点最大, 称之为大顶堆(或大根堆).

堆由程序员分配释放(比如: malloc,realloc,free), 若程序员不释放, 程序结束时由OS回收, 分配方式倒是类似于链表.

关于堆上内存空间的分配过程,首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

2.3 栈与堆的区别

堆与栈实际上是操作系统对进程占用的内存空间的两种管理方式,主要有如下几种区别:

  • 管理方式不同。栈由操作系统自动分配释放,无需我们手动控制;堆的申请和释放工作由程序员控制,容易产生内存泄漏
  • 空间大小不同。每个进程拥有的栈的大小要远远小于堆的大小。理论上,程序员可申请的堆大小为虚拟内存的大小,进程栈的大小64bits的Windows默认1M,64bits的Linux默认10M;
  • 生长方向不同。堆的生长方向向上,内存地址由低到高;栈的生长方向向下,内存地址由高到低。
  • 分配方式不同。堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是由操作系统完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由操作系统进行释放,无需我们手工实现。
  • 分配效率不同。栈由操作系统自动分配,会在硬件层级对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是由C/C++提供的库函数或运算符来完成申请与管理,实现机制较为复杂,频繁的内存申请容易产生内存碎片。显然,堆的效率比栈要低得多。
  • 存放内容不同。栈存放的内容,函数返回地址、相关参数、局部变量和寄存器内容等。当主函数调用另外一个函数的时候,要对当前函数执行断点进行保存,需要使用栈来实现,首先入栈的是主函数下一条语句的地址,即扩展指针寄存器的内存(eip),然后是当前栈帧的底部地址,即扩展基址指针寄存器内容(ebp),再然后是被调函数的实参等,一般情况下是按照从右向左的顺序入栈,之后是调用函数的局部变量,注意静态变量是存放在数据段或者BSS段,是不入栈的。出栈的顺序正好相反,最终栈顶指向主函数下一条语句的地址,主程序又从该地址开始执行。堆,一般情况堆顶使用一个字节的空间来存放堆的大小,而堆中具体存放内容是由程序员来填充的。

其他

C语言的内存模型分为5个区:栈区、堆区、静态区、常量区、代码区:

  1. 栈区:存放函数的参数值、局部变量等,由编译器自动分配和释放,通常在函数执行完后就释放了,其操作方式类似于数据结构中的栈。栈内存分配运算内置于CPU的指令集,效率很高,但是分配的内存量有限,比如iOS中栈区的大小是2M。
  2. 堆区:就是通过new、malloc、realloc分配的内存块,编译器不会负责它们的释放工作,需要用程序区释放。分配方式类似于数据结构中的链表。在iOS开发中所说的“内存泄漏”说的就是堆区的内存。
  3. 静态区:全局变量和静态变量(在iOS中就是用static修饰的局部变量或者是全局全局变量)的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后,由系统释放。
  4. 常量区:常量存储在这里,不允许修改。
  5. 代码区:存放函数体的二进制代码。

参考

CSDN: https://blog.csdn.net/ZHUO_SIR/article/details/80998631 博客园: https://www.cnblogs.com/cchHers/p/10010275.html

「真诚赞赏,手留余香」

Haoran Blog

真诚赞赏,手留余香

使用微信扫描二维码完成支付