递归调用栈溢出

递归的风险

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

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
 

 

    原文作者:边缘计算机
    原文地址: https://blog.csdn.net/panjunnn/article/details/117632070
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞