关于函数递归调用导致的StackOverflow那点事

本文涉及的产品
资源编排,不限时长
无影云电脑企业版,4核8GB 120小时 1个月
无影云电脑个人版,1个月黄金款+200核时
简介: 关于函数递归调用导致的StackOverflow那点事

 身为工程师,Stack Overflow这网站大家应该都是熟到不能再熟。如果没有他帮忙解决各种莫名其妙的错误或者bug,可能连设定个开发环境都要搞半天,更不用说要开发了,产出直接降低好几倍,不如直接下班算了。

image.gif编辑

但今天要谈的不是那个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;
}

image.gif

当然这写法很烂,你忍不住要吐槽,回到正题我只想让大家看看实际运行Sum(10)跟Sum(10000)也都有得到正确的结果。

但跑Sum(2000000)时就不是这样了,因为程序写太烂加上20000这数字太大,直接就原地炸开喷出Segmentation fault (core dumped),这就是所谓的Stack Overflow。

image.gif编辑

不出你的意料,他崩溃了,那么我们来讲解一下他的成因,首先看看到底什么是Stack。每个Process在启动时,系统都会分配一块內存给他使用,以最常见的内存布局来说,进程内的内存布局大概会长这样,由低位到高位主要分成Text、Data、Heap、Stack几个内存块。

最下面的Text内存块放的是要给CPU跑的可执行指令,一般来说你的代码越多,这部分就会越肥;而Data内存块放的就是进程内的全局、静态变量。

image.gif编辑

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)。

image.gif编辑

虽然栈可以动态增长,但他的大小也是有极限的,操作系统总不可能为了跑你的程序而把系统的内存都给你。

因此当你调用Sum(2000000)导致递归太深时,等待执行的函数又太多,但栈空间已经装不下更多帧,就会产生overflow的错误。

image.gif编辑

虽然一般来说都是递归没写好才会导致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;
}

image.gif

关于C,C++的内存布局

本片不在赘述,请看我之前的文章:

C++:28 --- C++内存布局(上)

C++:29 --- C++内存布局(下)

C++:30 ---C++类成员,成员函数的内存布局

相关文章
|
8月前
|
存储 编译器 Go
Golang底层原理剖析之函数调用栈-栈帧布局与函数跳转
Golang底层原理剖析之函数调用栈-栈帧布局与函数跳转
126 0
|
8月前
|
Go
golang力扣leetcode 462.最少移动次数使数组元素相等II
golang力扣leetcode 462.最少移动次数使数组元素相等II
73 0
|
8月前
|
C语言
【汇编语言实战】使用插入排序对给定的数组排序(用栈传递参数)
【汇编语言实战】使用插入排序对给定的数组排序(用栈传递参数)
60 1
|
8月前
|
算法 C语言
C语言函数递归调用详解与实战应用
C语言函数递归调用详解与实战应用
103 0
|
8月前
|
Java Go C++
Golang每日一练(leetDay0097) 顶端迭代器、寻找重复数
Golang每日一练(leetDay0097) 顶端迭代器、寻找重复数
69 0
Golang每日一练(leetDay0097) 顶端迭代器、寻找重复数
|
8月前
|
算法 安全 Java
【数据结构与算法】6、栈(Stack)的实现、LeetCode:有效的括号
【数据结构与算法】6、栈(Stack)的实现、LeetCode:有效的括号
49 0
|
存储 C语言 C++
【再识C进阶2(中)】详细介绍指针的进阶——函数指针数组、回调函数、qsort函数
【再识C进阶2(中)】详细介绍指针的进阶——函数指针数组、回调函数、qsort函数
|
测试技术
LeetCode-396 选转函数
LeetCode-396 选转函数
|
Python
如何写出你的第一个递归函数?
如何写出你的第一个递归函数?
90 0
|
存储 JavaScript
为什么我要说:柯里化 == 闭包+递归?
柯里化是 JS 高程中不可或缺的重心,本篇带你来冲一冲它!!