【问题标题】:Declaring a static variable inside a recursive function. Stack overflow在递归函数中声明一个静态变量。堆栈溢出
【发布时间】:2019-05-09 09:27:05
【问题描述】:

这段代码只是概念,没有任何意义。

void recur(int num)
{   
    static float tmp = num * num;
    if (num == 0)
    {
        return;
    }
    else
    {
        recur(num - 1);
    }
}

int main()
{
    recur(1000000);
}

我认为静态变量只使用内存中的一个位置,但是在 main 中对 recur 函数的调用导致堆栈溢出失败这确实有意义变量 tmp 是否在堆栈中声明,但它不是静态的堆栈,对吧?

tmp 变量的行为是什么??

谢谢

【问题讨论】:

  • 局部变量并不是堆栈中唯一的东西。每次调用也会产生开销(大多数情况下,可能会有例外)。
  • @Frank “也有每次调用的开销”加上一个局部变量 - num
  • 您是否尝试过启用优化?
  • 试试如果你删除静态变量会发生什么(可能会增加调用次数)

标签: c++ recursion static stack


【解决方案1】:

每次调用recur 函数都需要分配至少num 参数,这会占用自动存储空间。

【讨论】:

  • "你的函数调用需要分配"不,它不会,它可以分配但不需要。
  • @Slava 任何关于“它可以分配但不是必需的”的引用?
  • 好吧,由于 as-if 规则,每个 num 可能存在也可能不存在。它只需要表现得好像它已经存在一样。一种可能性是尾调用优化,在这种情况下,递归被转换为循环。我不确定这是否是@Slava 的意思,但这是递归实际上可能不需要增加存储空间的一个可能示例。编辑:虽然我不确定在实践中引入static 变量时编译器的智能或受限程度。实际上,该函数没有可观察到的行为,可以完全优化,这可能不是尾调用。
  • @FrançoisAndrieux 从技术上讲,recur(1000000); 可以完全删除,因为它没有任何副作用。但这将是“可以优化但不需要”的情况。
  • 显而易见的答案是参数num 可以在一个寄存器中传递,其值将在递归调用之前递减。使用退货地址并非易事。
【解决方案2】:

我认为静态变量只是使用内存中的一个位置

你想的没错。

tmp 变量的行为是什么??

tmp 变量只有一个对象。所有对recur 的调用都使用同一个对象。它在第一次调用函数时被初始化,并在main 返回后销毁。


每个函数调用都会将堆栈向下推 - 除非该调用已通过扩展内联调用(例如尾调用优化)进行优化 - 并且深度递归很容易溢出堆栈。

请注意,您的函数还有一个参数,该参数具有自动存储功能,可能会加快堆栈的消耗。

【讨论】:

  • 参数可以很容易地传入寄存器,但是返回地址不能
【解决方案3】:

静态变量不在堆栈中。您可以将其视为全局变量。出现堆栈溢出是因为每次调用都将参数“num”压入堆栈。另外,返回地址是放在栈上的,所以即使是“空”的函数也会导致溢出(图片来自https://en.wikipedia.org/wiki/Call_stack

【讨论】:

  • tbh 我自己不知道确切的规则,如果可能的话,我只是避免使用其他人的内容,对于维基百科,我想只要你清楚你从哪里得到它就可以了
【解决方案4】:

在 C 和 C++ 中来自函数的静态变量,而全局变量存储在与堆栈和堆分开的内存的 .data 部分中。

在 WiKi 中有一个不错的diagram

正如之前的答案和 cmets 所指出的,每次您调用 recur 时,都会分配一个返回地址的位置,并且可能(取决于平台,我假设您正在 x86 上测试)num 参数分配在堆栈。

您可以使用较小的递归(例如 10 次迭代)并打印出地址以查看部分之间的差异,如下所示:

#include <iostream>

int x;

void recur(int num)
{   
    static float tmp = num * num;

    std::cout << "tmp: " << &tmp << " num: " << &num << std::endl;
    if (num == 0)
    {
        return;
    }
    else
    {
        recur(num - 1);
    }
}

int main()
{
    int z;
    int *p = new int;
    std::cout << "p: " << p << " x: " << &x << " z: " << &z << std::endl;
    recur(10);

    delete p;
    return 0;
}

示例输出如下所示:

p: 0x1c12c20 x: 0x6011b8 z: 0x7ffc0b6dd914
tmp: 0x6011c8 编号: 0x7ffc0b6dd8fc
tmp: 0x6011c8 编号: 0x7ffc0b6dd8dc
tmp: 0x6011c8 编号: 0x7ffc0b6dd8bc
tmp: 0x6011c8 编号: 0x7ffc0b6dd89c
tmp: 0x6011c8 编号: 0x7ffc0b6dd87c
tmp: 0x6011c8 编号: 0x7ffc0b6dd85c
tmp: 0x6011c8 编号: 0x7ffc0b6dd83c
tmp: 0x6011c8 编号: 0x7ffc0b6dd81c
tmp: 0x6011c8 编号: 0x7ffc0b6dd7fc
tmp: 0x6011c8 编号: 0x7ffc0b6dd7dc
tmp: 0x6011c8 编号: 0x7ffc0b6dd7bc

【讨论】:

    【解决方案5】:

    我看过各大编译器的汇编,最大限度的优化你应该不会有任何堆栈溢出:

    • GCC:简化函数返回0,不递归。
    • Clang:作为 GCC
    • MSVC:使用内部跳转执行递归调用(尾调用的一种),只有第一个非递归调用的参数被压入堆栈。

    所以我猜如果你为这个琐碎的情况打开优化,你不会有堆栈溢出。

    但我想你的情况更复杂。

    如果您有一个函数在没有任何静态变量的情况下不会导致任何堆栈溢出,然后在添加此静态变量时导致堆栈溢出,原因可能是静态变量初始化的代码生成的副作用。

    静态变量初始化由一种“互斥锁”保护,以确保静态变量在多线程程序中只初始化一次。这些互斥体导致调用库函数。在调用函数之前,编译器必须确保调用者保存的、未使用的参数寄存器和被调用者保存的寄存器(它不会使用)在调用中得到保留。为此,编译器可能会将寄存器值压入堆栈。

    因此,即使静态变量初始化在程序流程中只发生一次,它的简单存在也会极大地改变生成的代码。

    【讨论】:

      猜你喜欢
      • 2017-10-20
      • 1970-01-01
      • 2011-02-26
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多