堆(heap)和栈(stack)的概念我们经常在程序里面见到,当然一般分两种场合:数据结构和内存相关,两种场合下,概念有些不同。这里我们说的堆栈的概念指的是后者,即内存相关的场景。

1. 内存结构简介

堆和栈其实都是内存的一部分,只是他们的使用有些不同而已。所以,在探讨二者之前我们先简单看一下与程序相关的内存划分。

memory1 图 1 概况图

memory2

图2 详细图

图1为一个简要图,简单来说我们可以认为内存被划分为四块区域(两幅图中上边为内存高地址,下边为内存低地址):

  • 栈区:由编译器自动分配与释放(故不会产生内存泄露),存放函数的参数值、局部变量等。
  • 堆区:由程序员负责分配与释放,可能产生内存泄露。
  • 数据段(又称为全局区或者静态区)——由只读数据段、初始化的读写数据段、未初始化数据段(BSS)组成。(1)只读数据段:只读数据段是程序使用的一些不会被更改的数据,使用这些数据的方式类似查表式的操作,由于这些变量不需要更改,因此只需要放置在只读存储器中即可。一般是const修饰的变量以及程序中使用的文字常量一般会存放在只读数据段中。(2)初始化的数据段:已初始化数据是在程序中声明,并且具有初值的变量,这些变量需要占用存储器的空间,在程序执行时它们需要位于可读写的内存区域内,并且有初值,以供程序运行时读写。在程序中一般为已经初始化的全局变量,已经初始化的静态局部变量(static修饰的已经初始化的变量)。即该区域存储初始化过的全局变量和初始化过的静态变量。(3)未初始化数据段:未初始化数据是在程序中声明,但是没有初始化的变量,这些变量在程序运行之前不需要占用存储器的空间。与读写数据段类似,它也属于静态数据区。但是该段中数据没有经过初始化。未初始化数据段只有在运行的初始化阶段才会产生,因此它的大小不会影响目标文件的大小。在程序中没有初始化的全局变量和没有初始化的静态局部变量都存储在这里。
  • 代码段:存放函数体的二进制代码。

2. 堆与栈的区别

2.1 申请方式

  • 栈:由系统自动分配与维护,所以申请效率较高(申请速度快)。例如,在程序中声明一个局部变量int a,则系统自动在栈中为a开辟一个空间。
  • 堆:由程序员自己申请(malloc,realloc,calloc)与维护。申请效率低(申请速度慢)且可能产生内存泄露,但使用起来比较方便与灵活。例如,
    char *p;
    p = (char *)malloc(sizeof(char));

    需要注意的是虽然申请的空间(即p指向的内存空间)在堆里面,但是p本身仍存储在栈里面,因为它是一个局部变量。

2.2 申请大小限制

  • 栈:在Windows下栈是向低地址扩展的数据结构,是一块连续的内存区域(它的生长方向与内存的生长方向相反)。栈的大小是固定的。如果申请的空间超过栈的剩余空间时,将提示overflow。
  • 堆:堆是高地址扩展的数据结构(它的生长方向与内存的生长方向相同),是不连续的内存区域,可能产生内存碎片。这是由于系统使用链表来存储空闲内存地址的,自然是不连续的,而链表的遍历方向是由底地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。

2.3 系统响应

  • 栈:只要栈的空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
  • 堆:首先应该知道操作系统有一个记录空闲内存地址的链表,但系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的free语句才能正确的释放本内存空间。另外,找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。对于堆来讲,对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题。

2.4 存储的内容

  • 栈:在函数调用时,第一个进栈的主函数中后的下一条语句的地址,然后是函数的各个参数,参数是从右往左入栈的,然后是函数中的局部变量。注:静态变量是不入栈的。当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续执行。
  • 堆:一般是在堆的头部用一个字节存放堆的大小。

2.5 存取效率

  • 栈:char s1[]="time-track.cn.";是在运行时赋值的;用数组比用指针速度更快一些,指针在底层汇编中需要用edx寄存器中转一下,而数组在栈上读取。
  • 堆:char *s1="time-track.cn";是在编译是就确定的。

在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。

比如下面的例子:

#include <stdio.h>    
int main()    
{    
    char   a = 1;    
    char   c[] = "1234567890";    
    char   *p = "1234567890";    
    a = c[1];    
    a = p[1]; 
   
    return 0;    
}

对应的汇编代码:

10:   a   =   c[1];    
00401067   8A   4D   F1   mov   cl,byte   ptr   [ebp-0Fh]    
0040106A   88   4D   FC   mov   byte   ptr   [ebp-4],cl    
11:   a   =   p[1];    
0040106D   8B   55   EC   mov   edx,dword   ptr   [ebp-14h]    
00401070   8A   42   01   mov   al,byte   ptr   [edx+1]    
00401073   88   45   FC   mov   byte   ptr   [ebp-4],al

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

说明:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。

2.6 分配方式

  • 栈:栈有静态分配和动态分配两种方式。静态分配是由编译器完成的,比如局部变量的分配。动态分配是由alloca 函数完成的,但栈的动态分配与堆不同,它的释放是由编译器负责的。
    The alloca() function allocates size bytes of space in the stack frame of the caller. This temporary space is automatically freed when the function that called alloca() returns to its caller.
  • 堆:都是动态分配的,没有静态分配的堆。

本文参考自网上多篇文章,此处不一一列举。