身为工程师,Stack Overflow这网站大家应该都是熟到不能再熟。如果没有他帮忙解决各种莫名其妙的错误或者bug,可能连设定个开发环境都要搞半天,更不用说要开发了,产出直接降低好几倍,不如直接下班算了。
编辑
但今天要谈的不是那个Stack Overflow,而是要讲程序在使用內存时,因为调用栈的堆太高了不小心把內存用完,所产生的overflow。
认真写个stack overflow
先来看看以下这个用C写成的sum(n),他做的事情很简单,就只是用递回来计算从1到n的总和,譬如说sum(3)= 1 + 2 + 3 = 6。
int Sum(int n) { if (n == 1) { return 1; } return Sum(n - 1) + n; }
当然这写法很烂,你忍不住要吐槽,回到正题我只想让大家看看实际运行Sum(10)跟Sum(10000)也都有得到正确的结果。
但跑Sum(2000000)时就不是这样了,因为程序写太烂加上20000这数字太大,直接就原地炸开喷出Segmentation fault (core dumped),这就是所谓的Stack Overflow。
编辑
不出你的意料,他崩溃了,那么我们来讲解一下他的成因,首先看看到底什么是Stack。每个Process在启动时,系统都会分配一块內存给他使用,以最常见的内存布局来说,进程内的内存布局大概会长这样,由低位到高位主要分成Text、Data、Heap、Stack几个内存块。
最下面的Text内存块放的是要给CPU跑的可执行指令,一般来说你的代码越多,这部分就会越肥;而Data内存块放的就是进程内的全局、静态变量。
编辑
Text跟Data这两个内存块基本上是固定大小,因为你写的程序一旦编译好,全局变量的数量就会固定下来,不太会动态增长。但Heap跟Stack就不一样了,当程序有用到动态內存配置像是C语言的malloc/free那些空间就会被分配在Heap里面。
而最上面的Stack也就是这篇的重点,则是放函数调用时会用到的一些东西,像是传进来的参数、局部变量等等,每个函数被调用的时候会有一个stack frame被创造出来,结束时再马上释放掉。
深层递归发生了什么事?
那递归是怎么让栈爆掉的呢?以我们的例子来说,当我们调用Sum(3)时,他内部会先调用Sum(2),而Sum(2)又再调用Sum(1),因此整个栈里面会有多个栈帧依序叠起来,只有当最上层的Sum(1)执行结束了之后,才有办法继续执行下面的Sum(2)跟Sum(3)。
编辑
虽然栈可以动态增长,但他的大小也是有极限的,操作系统总不可能为了跑你的程序而把系统的内存都给你。
因此当你调用Sum(2000000)导致递归太深时,等待执行的函数又太多,但栈空间已经装不下更多帧,就会产生overflow的错误。
编辑
虽然一般来说都是递归没写好才会导致Stack Overflow,但其实Stack Overflow并不一定是递归造成的,只要你的call stack够深(真的要非常非常深),那就可能会有这个。
如何修改栈溢出
若真的不幸遇到Stack Overflow要怎么解决呢?第一步就是先检查递归的终止条件。像这边如果没有在n == 1时return 1,那就算只是跑个Sum(3)也会炸掉。
int Sum(int n) { int s = 0 for (i = 1; i <= n; i++) { s += i; } return s; }
关于C,C++的内存布局
本片不在赘述,请看我之前的文章: