程序在内存中的分布
1. 内存地址
在现代的操作系统中,当我们说到内存,往往需要分两部分来讲:物理内存 和 虚拟内存。从硬件上讲,虚拟空间是 CPU 内部的寻址空间,位于 MMU 之前,物理空间是总线上的寻址空间,是经过 MMU 转换之后的空间。
虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。目前,大多数操作系统都使用了虚拟内存,如 Windows 家族的"虚拟内存";Linux 的"交换空间"等。
一般我们所说的程序在内存中的分布指的就是程序在虚拟内存中的存储方式从低地址到高地址,可分为下面几段:
| 区域 | 功能 |
|---|---|
| 预留内存地址 | 操作系统维护的内存地址,不可访问 |
| 程序代码区 | 只读,存代码和一些其他的东西 |
| data段 | 存初始化的全局变量和static变量,另外还有文字常量区,常量字符串就是放在这里,程序结束后有系统释放 |
| bss段 | 存未初始化的全局变量和 static 变量(局部静态变量可以不初始化,默认为 0) |
| 堆 | 由低地址向高地址增长,一般 new 和 malloc 分配,由程序员分配释放 |
| 栈 | 由高地址向低地址增长,和堆的增长方式相对,对不同的 OS 来说,栈的初始大小有规定,可以修改,目前默认一般为 2M,由编译器自动分配释放 |
在上面存的都是操作系统和内核调用的一些内存地址

2. 堆和栈的不同
2.1 分配方式不同
栈: 由系统自动分配。 例如,声明在函数中一个局部变量 int b; 系统自动在栈中为 b 开辟空间。
堆: 需要程序员自己申请,并指明大小,在c中 malloc 函数。
如 p1 = (char *)malloc(10); 在 C++ 中用 new 运算符
如 p2 = (char *)new(10); 但是注意 p1、p2 本身是在栈中的。
2.2 空间大小不同
一般来讲在 32 位系统下,堆内存可以达到 4G 的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在 VC6 下面,默认的栈空间大小是 1M。
2.3 分配效率不同
栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是 C/C++ 函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考 数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。
2.4 能否产生碎片不同
栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时, 会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的 delete 语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
对于堆来讲,频繁的 new/delete 势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出。
2.5 生长方向不同
对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。
堆和栈相比,由于大量 new/delete 的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址,EBP 和局部变量都采用栈的方式存放。所以,我们推荐大家尽量用栈,而不是用堆。虽然栈有如此众多的好处,有时候分配大量的内存空间,还是用堆好一些。