堆栈堆栈,什么是堆?什么又是栈?什么又是堆栈?
它们的底层分配机制又是怎么样的?
此文我将带着你从内存布局的角度弄懂程序的堆和栈。

程序运行时的内存布局
当我们写好的程序被运行时,操作系统会将程序加载到内存中并为程序分配虚拟地址空间。
同时加载器会为程序分配以下几个内存区域。从低地址开始,首先是代码段,接着是数据段。
接下来是堆空间,堆空间在程序运行时动态分配,它的增长方向是向高地址增长。
然后是栈空间,栈空间也是在程序运行时分配,从地址空间的高地址开始,向低地址增长。
C++中栈内存与堆内存的分配方式
首先我们从代码的角度来理解如何在C++中分配堆和栈内存空间。
首先我们定义一个INT型变量VAR1,并赋值6:
int var1 = 6;
它会在栈空间分配一个占用4个字节的int。
但如果我们需要在堆上分配一个同样类型的内存,我们需要这样做:
int *pvar1 = new int;
*pvar1 = 6;
在堆上分配内存时,我们使用了NEW操作符。
数组在栈与堆上的分配对比
现在我们再来在栈空间上分配一个5个元素的INT型数组,我们需要这样写:
int array[5];
array[0] = 1;
array[1] = 2;
array[2] = 3;
array[3] = 4;
array[4] = 5;
那如果需要在堆空间上申请一个5个元素的INT型数组,我们需要这样做:
int *parray = new int[5];
parray[0] = 1;
parray[1] = 2;
parray[2] = 3;
parray[3] = 4;
parray[4] = 5;
变量分配示例与内存观察
我们在栈空间再分配一个INT型变量,并赋值为7:
int var2 = 7;
然后再在堆空间分配一个INT型变量,也赋值为7:
int *pvar2 = new int;
*pvar2 = 7;
现在我们在这里设置一个断点,然后运行程序。
我们拷贝变量var1,然后把它粘贴到内存窗口的地址栏中,并在前边加上取地址符&,这样我们就能把这个变量的地址取出来并查看它的内存 &var1。
可以看到,它内存的值是06。
然后我们按F10单步运行程序下一行,如果你看过我之前关于内存的视频,你应该会知道,此时单步运行了一行,内存中的值应该被修改了。
那么内存应该变成红色,但是现在看到内存中并没有显示红色的地址,这是为什么呢?
其实,你只需要把内存窗口往上滚一滚,也就是向内存的低地址滚动一点。看到了吧,它前边的内存被修改了。
好,我们再继续按F10单步走一行,现在这里被修改为2。
我们继续单步运行,把后边几行运行完。看到了吧,它是往低地址的内存空间去填充的。
栈内存的分配特点
那么到这里你应该明白了一点,栈空间的内存分配方向是从内存的高地址向低地址分配的,并且栈内存的分配是连续的。
补充一点,也许细心的你也发现了在这些变量之间还夹杂着一些CC的未初始化的内存,其实这些是调试模式下调试器为了防止内存溢出所添加的。调试器通过检测这些内存区的变化,帮助你快速发现问题。
堆内存的分配特点
好,现在我们再来看看堆内存的分配情况,我们继续运行程序。
这里是为了一个INT申请堆内存,它占用4个字节。我们看一下它的内存地址是0x014fa560。
我们继续运行,这一行申请另一块堆空间, 它的内存地址是0x0141a650。
从这2个地址对比来看,它们并没有连续在一起。
我们再往下运行,pvar2申请到的内存地址是0x0141a690。
那到这里你就能明白了,堆内存并不会连续分配,每次申请到的内存地址可能会相差很远,并且它是从低地址向高地址分配的。
汇编层面看堆栈分配差异
好,现在,我们进入到汇编模式下看看。
可以看到,我们这条对栈空间的var1变量的操作只有简单的一行汇编指令就完成了,为什么会这么简单呢?
这是因为 var1 它分配在栈空间上,对它进行读取操作时,其实只需要用一个相对于 栈帧基址的偏移量就可以了。
我们再看看下边申请堆内存的代码,可以看到一条NEW代码,有这么多行汇编,而且它还要再调用一次NEW操作符。
这是因为在堆内存的分配时需要支持动态大小,分配器需要找到足够大的空闲内存块。这涉及复杂的查找算法,例如 首次适配、最佳适配 或 分割空闲块 等。
同时堆分配需要从用户态切换到内核态执行。并最终依赖系统调用(如 brk 或 mmap)来扩展堆空间。系统调用的代价很高。
所以堆内存操作相对栈内存来说是非常耗时的,这也是为什么在高性能程序开发时需要采用内存池的原因。
常见误区与使用注意事项
在早期的计算机书籍中,由于翻译的问题,有时会用堆栈来称呼栈,给后来学习者带来了一些困惑。
其实堆(Heap)和栈(Stack)是两种不同的内存分配方式,务必要弄清楚。
另外默认情况一下,编译器分配给线程的栈空间是1M大小,如果你分配一个过大的临时变量则会导至栈溢出。
当然你也可以在编译器中指定默认的线程栈空间大小,但不建议这么做,如果需要更大的内存空间,应该考虑使用堆内存。
最后,当我们使用完堆内存的时候,一定要调用DELETE或者FREE来释放内存,不然就会造成内存泄漏。
但是栈内存,当代码的作用域结束时,它就会自动释放,不需要担心内存泄漏。
总结
关于堆和栈,现在你有更深入的认识了吗?


