递归调用栈溢出

本文探讨了递归在实际开发中的风险,包括栈溢出和理解复杂性,并通过示例解释了栈溢出的原因,如死循环、局部变量过大和递归深度过深。提出了评估栈空间和优化递归的方法,如控制递归深度、使用尾递归以及用循环替代递归。同时,强调了尾递归在优化中的作用,虽然其开销小于普通递归,但深度过大仍可能导致问题。建议在必要时使用递归,并优先考虑尾递归和循环解决方案。

递归的风险

实际开发中应避免使用递归,原因主要两点:

1. 递归调用在深度上不可预测,层数过多不断压栈,可能会引起栈溢出的崩溃;
2. 不容易理解;

 

栈溢出

stack overflow异常是程序中常常会碰到的,原因是调用线程栈不够用。windows默认栈大小是1M,使用栈空间超过了1M就会报出stack overflow异常。

产生原因

1、死循环

出现了死循环,例如:递归函数没有出口,不管栈空间多大必定溢出。

long  func(int n)
{
     return n*func(n-1);
}

这是个没有出口的递归函数, 必然引起栈溢出。纠正:

long func(int n)
{
     if(n==1)   return 1;
     return   n*func(n-1);
}

2、栈确实不够用

存到栈上的主要内容是:局部变量和函数调用的函数环境,包括函数参数等。

局部变量,例如:

char buff[1024*1024]

buff为1M,如果你的栈默认也是1M大小,这就会发生栈溢出,因为其他东西会占掉少量栈空间,局部变量能用的空间肯定小于1M,程序在执行到main函数之前,就会跳出stack overflow异常。

3、函数调用层数太深

这种情况一般发生在递归调用中

过多的递归调用为什么会引起栈溢出呢?

在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当程序执行进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,就会导致栈溢出。函数的参数是通过stack栈来传递的,在调用中会占用线程的栈资源。递归调用在到达最后的结束点后,函数才能依次退栈清栈,如果递归调用层数过多,就可能导致占用的栈资源超过线程的最大值,从而导致栈溢出,程序异常退出。

不得不使用递归的场景:遍历一个目录下的所有文件,包括其子目录的文件。
对于解决一些包含重复类似逻辑的问题,递归对于开发人员来说是清晰的选择。

在不得不使用递归时,如何评估栈空间是否足够,避免栈溢出。

评估思路:

1. 确认当前线程栈空间限制是多少?

2. 递归调用n次,分析n次压栈后栈空间的损耗是多少?

3. 结合业务预估最大可能的递归调用次数,如果大于或已经接近栈大小, 就存在栈越界风险,需要放大栈空间或者做功能规格约束。

优化

当栈不够使用时,一种办法是修改程序:

主要还是要注意递归调用引起的栈溢出,多数情况可以通过算法优化来解决:

1、控制递归深度。例如,使用动态规划来代替递归算法等。

2、修改栈的大小。

 

尾递归优化

尾递归是指,在函数返回的时候,调用函数本身,并且return语句不能包含表达式。如果递归调用,都出现在函数的末尾,这个递归函数就是尾递归的函数。

尾递归函数的特点是在回归过程中,不用做操作,这个特性很重要,因为大多数现代编译器会利用这一特点,自动生成优化的代码。
有些语言极力提倡尾递归,因为它们的编译器会对代码进行优化,不会因为递归次数的增加,给函数栈带来巨大的开销。

尾递归优化,使无论调用多少次,都只占用一个栈帧,不会出现栈溢出的情况。

递归的优点是逻辑清晰。

 

循环替代递归

所有的递归都可以改写成循环的方式。 

斐波那契数列: 1,1,2,3,5,......
求斐波那契数列的第N项的值

尾递归和循环的执行效率都非常高。但是尾递归的递归层数大到一定程度会出现段错误。尾递归的函数栈开销比普通递归要小的多,执行效率大很多。 但是尾递归仍然有函数栈开销。
正因为尾递归具有函数栈开销,其调用次数比循环小很多。

实现一个功能,能不用递归就别用递归,能用尾递归就用尾递归。

 //一般递归
int fib_normal(int n)
{
    if (n <= 2)
        return 1;
    else
        return fib_normal(n-1) + fib_normal(n-2);
}
 
//尾递归
int fib_rail(int n, int first,  int second)
{
    if (n == 1) return first;
    if (n == 2) return second;
    return fib_rail(n-1, second, second+first);
}
unsigned int fib_rail_rec(unsigned int n)
{
    return fib_rail_rec(n, 1, 1);
}
 
// 循环取代递归
int fib_no(int n)
{
    if (n <= 2)
        return 1;
    int x=1, y=1, y_tmp=0;
    for (int i=0; i<n-2; i++)
    {
        y_tmp = y;
        y = x+y;
        x = y_tmp;
    }
    return y;
}

栈溢出windbg显示Stack overflow

0:072> !analyze -v

CONTEXT:  (.ecxr)
eax=0000000e ebx=76fbceb8 ecx=00413fb8 edx=00417b98 esi=00413fb8 edi=00000000
eip=76ed83eb esp=2f652ffc ebp=2f653040 iopl=0         nv up ei pl nz ac pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010216
ntdll!RtlAcquireSRWLockExclusive+0xb:
76ed83eb 53              push    ebx
Resetting default scope

EXCEPTION_RECORD:  (.exr -1)
ExceptionAddress: 76ed83eb (ntdll!RtlAcquireSRWLockExclusive+0x0000000b)
   ExceptionCode: c00000fd (Stack overflow)
  ExceptionFlags: 00000000
NumberParameters: 2
   Parameter[0]: 00000001
   Parameter[1]: 2f652ff8
 



 

### 递归调用是否容易导致堆栈溢出 递归是一种常见的编程技术,通过函数调用自身来解决问题。虽然递归在解决某些问题时非常方便,但如果递归的深度过大,可能会导致堆栈溢出。堆栈溢出通常发生在递归调用没有合适的终止条件或递归深度过大时,导致 JVM 的调用栈空间耗尽[^1]。 #### 堆栈溢出的原因 通常“堆栈溢出”是指“调用堆栈(call stack)的溢出”。要通俗地解释调用堆栈可能比较困难,因为它涉及许多其他计算机架构的知识。而这个答案只是简单地解释堆栈这种数据结构的特点——先进后出/后进先出。溢出是指这个数据结构满溢,不能存放更多数据。其他的数据结构也会遇到这个情况。即使数据结构并非固定容量,而是可扩展的,在有限的内存空间下仍是有满溢的机会[^3]。 #### 示例代码分析 以下是一个简单的递归函数示例: ```c int stack_test(int n) { int x[10000]; if(n == 1) return 1; return stack_test(n-1); } int _tmain(int argc, _TCHAR* argv[]) { printf("%d\n", stack_test(50)); getchar(); return 0; } ``` 当调用 `stack_test(50)` 时,不会产生堆栈溢出。如果调用 `stack_test(100)` 时,则会产生。另一个示例中,`stack_test()` 函数中没有声明很大的局部变量,但是递归次数太多,也会产生堆栈溢出[^4]。 #### 解决方法 引起堆栈溢出的原因是,分配了太多的函数指针、变量指针和参数,以致在堆栈里申请的内存数量不够用。到目前为止,堆栈溢出最平常的原因是无终止的递归。用户可以编写自己的代码以检测和防止堆栈溢出。例如,如果应用程序依赖于递归,则可以使用计数器或状态条件来终止递归循环[^4]。 --- ### 替代方案与最佳实践 为了避免堆栈溢出,可以考虑将递归算法转换为迭代算法。例如,可以使用循环的方式来解决,将需要的数据在关键的调用点保存下来使用。简单地说,就是用自己的数据保存方法来代替系统递归调用产生的堆栈数据[^2]。 --- ### 相关问题 1. 如何在 C 语言中使用 `srand()` 函数提升 `rand()` 的随机性? 2. PHP 中除了 `array_rand()` 是否还有其他生成随机验证码的方法? 3. 为什么 `rand()` 函数不适合用于高安全性要求的验证码生成?
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值