C中的无限递归

时间:2013-08-14 08:49:21

标签: c recursion

鉴于C程序具有无限递归:

int main() {

    main();

    return 0;
}

为什么会导致堆栈溢出。我知道这会在C ++中导致来自以下线程Is this infinite recursion UB?的未定义行为(并且作为侧节点,不能在C ++中调用main())。但是,valgrind告诉我这会导致堆栈溢出:

Stack overflow in thread 1: can't grow stack to 0x7fe801ff8

然后最终程序因分段错误而结束:

==2907== Process terminating with default action of signal 11 (SIGSEGV)
==2907==  Access not within mapped region at address 0x7FE801FF0

这在C中是否也是未定义的行为,或者这是否真的会导致堆栈溢出,为什么会导致堆栈溢出?

修改

1我想知道C中是否允许无限递归?
2这是否会导致堆栈溢出? (已得到充分回答)

15 个答案:

答案 0 :(得分:16)

每当调用一个函数时,参数都被压入堆栈,这意味着堆栈段上的数据被“分配”。调用该函数时,返回地址也会被CPU压入堆栈,因此它知道返回的位置。

在你的示例中,这意味着没有使用任何参数,因此唯一推送的是返回地址,它相当小(x86-32 architexture上为4个字节),另外还调整了堆栈帧这个架构上的另外四个字节。

由此可见,一旦堆栈段耗尽​​,该函数就不能被调用,并且会向OS引发异常。现在可能发生两件事。操作系统将异常转发回您的应用程序,您将看到堆栈溢出。或者OS可以尝试为堆栈segemnt分配额外的空间,直到达到定义的限制,之后应用程序将看到堆栈溢出。

所以这段代码(我把它重命名为infinite_recursion()作为main()不能被调用)...

int inifinite_recursion(void)
{
    inifinite_recursion();
    return 0;
}

......看起来像这样:

_inifinite_recursion:
    push    ebp                    ; 4 bytes on the stack
    mov ebp, esp

    call    _inifinite_recursion   ; another 4 bytes on the stack
    mov eax, 0                 ; this will never be executed.

    pop ebp
    ret 

<强>更新

关于定义递归的标准C99,到目前为止我发现的最好的是6.5.2.2节第11段:

  
    

应允许递归函数调用,直接或间接通过任何其他函数链。

  

当然,这并没有回答它是否定义了堆栈溢出时会发生什么。但是至少它允许递归调用main,而这在C ++中被明确禁止(第3.6.1节第3段和第5.2.2节第9段)。

答案 1 :(得分:10)

程序是否无限地递归 是不可判定的。没有明智的标准会要求一个甚至可能无法验证合规程序的属性,因此没有C标准,当前或未来,对于无限递归都没有任何说法(就像没有C标准一样)将要求合规程序最终停止。)

答案 2 :(得分:5)

递归是一种迭代,在移动到下一次迭代之前隐式保留本地状态。通过考虑一个接一个地相互调用的常规函数​​来解释这一点很容易:

void iteration_2 (int x) {
    /* ... */
}

void iteration_1 (int x) {
    if (x > 0) return;
    iteration_2(x + 1);
}

void iteration_0 (int x) {
    if (x > 0) return;
    iteration_1(x + 1);
}

每个iteration_#()基本上彼此相同,但是每个x都有自己的void iteration (int x) { if (x > 0) return; iteration(x + 1); } ,并且每个都记得哪个函数调用了它,所以它可以在函数时正确返回给调用者它调用已完成。当程序转换为递归版本时,这个概念不会改变:

if

如果停止条件(函数的return检查到x)被删除,则迭代变为无限。递归没有返回。因此,为每个连续的函数调用(本地int iteration () { return iteration(); } 和调用者的地址)记住的信息不断堆积,直到操作系统内存不足以存储该信息。

可以实现一个不会溢出“堆栈”的无限递归函数。在足够的优化级别,许多编译器可以应用优化来删除记住尾递归调用的任何内容所需的内存。例如,考虑一下程序:

gcc -O0

使用iteration: .LFB2: pushq %rbp .LCFI0: movq %rsp, %rbp .LCFI1: movl $0, %eax call iteration leave ret 编译时,它变为:

gcc -O2

但是,当使用iteration: .LFB2: .p2align 4,,7 .L3: jmp .L3 进行编译时,将删除递归调用:

return

这种无限递归的结果是一个简单的无限循环,并且“堆栈”不会溢出。因此,允许无限递归,因为允许无限循环。

然而,您的程序不适合尾部调用优化,因为递归调用不是您的函数执行的最后一项操作。您的函数仍然具有跟随递归调用的{{1}}语句。由于在递归调用返回后仍有代码需要执行,因此优化器无法消除递归调用的开销。它必须允许调用正常返回,以便后面的代码可以执行。因此,您的程序将始终支付存储调用代码的返回地址的惩罚。

该标准在任何特定术语中都不代表“无限递归”。我已经收集了我认为与您的问题相关的内容。

  • 允许递归调用函数(C.11§6.5.2.2¶11)
  

应允许递归函数调用,直接或间接通过任何其他函数链。

  • 递归进入语句会创建局部变量的新实例(C.11§6.2.4¶5,6,7)
  

声明标识符的对象,没有链接且没有存储类   speci fi er static具有自动存储持续时间,一些复合文字也是如此。 ...

     

对于没有可变长度数组类型的对象,其生命周期会延长   从进入与之关联的块直到该块的执行结束   无论如何。 (输入一个封闭的块或调用一个函数暂停,但不会结束,   执行当前块。)如果以递归方式输入块,则执行新的实例   每次都创建对象。 ...

     

对于具有可变长度数组类型的对象,其生命周期从   对象的声明,直到程序的执行离开了范围   宣言。如果以递归方式输入范围,则会创建该对象的新实例   每一次。

该标准讨论了许多地方的内存分配失败,但从不在具有自动存储持续时间的对象的上下文中。未在标准中明确定义的任何内容都是未定义的,因此无法分配具有自动存储持续时间的对象的程序具有未定义的行为。这同样适用于只有很长的函数调用链或太多递归调用的程序。

答案 3 :(得分:3)

无论何时进行函数调用(包括main()),函数调用“info”(例如参数)都会被推到堆栈顶部。函数返回时,此信息将从堆栈中弹出。但正如您在代码中看到的那样,在返回之前对主进行递归调用,因此堆栈会不断增长,直到达到其限制并因此导致分段错误。

堆栈的大小通常是有限的,并且在运行之前决定(例如通过操作系统)。

这意味着堆栈溢出不仅限于main(),而是限于任何其他递归函数,而没有适当的方法来终止它的树(即基本情况)。

答案 4 :(得分:2)

即使函数不使用堆栈空间用于局部变量或参数传递,它仍然需要存储返回地址和(可能)帧的基指针(使用gcc,这可以通过-fomit-frame-pointer禁用)

在足够高的优化级别上,如果tail-call optimization适用,编译器可能能够将递归重写为循环,这样可以避免堆栈溢出。

答案 5 :(得分:2)

解决问题1

  

我想知道C中是否允许无限递归?

本文Compilers and Termination Revisited John Regehr回答C standard是否允许无限递归,在梳理完标准后,对我来说结论是不是太令人惊讶暧昧。本文的主要内容是关于无限循环以及是否支持各种语言(包括CC++)的标准来进行非终止执行。据我所知,讨论也适用于无限递归,当然假设我们可以避免堆栈溢出。

C++1.10 Multi-threaded executions and data races中所述的24不同:

The implementation may assume that any thread will eventually do one of the
following:
  — terminate,
  [...]

这似乎排除了C++中的无限递归。 draft C99 standard部分6.5.2.2 Function calls11中说明了{{3}}:

  

应允许递归函数调用,直接或间接通过任何其他函数链。

对递归没有任何限制,并在5.1.2.3 Program execution5中说明了这一点:

The least requirements on a conforming implementation are:
— At sequence points, volatile objects are stable in the sense that previous 
  accesses are complete and subsequent accesses have not yet occurred.
— At program termination, all data written into files shall be identical to the
  result that execution of the program according to the abstract semantics would
  have  produced.
— The input and output dynamics of interactive devices shall take place as
  specified in 7.19.3. The intent of these requirements is that unbuffered or     
  line-buffered output appear as soon as possible, to ensure that prompting
  messages actually appear prior to a program waiting for input.

正如文章所说,第一个条件应该是直接满足,根据文章的第三个条件并不真正涵盖终止。所以我们留下了第二个要处理的条件。根据该文章的含义不明确,文章的引用如下:

  

如果它是在讨论在抽象机器上运行的程序的终止,那么它是空洞的,因为我们的程序没有终止。如果它正在讨论由编译器生成的实际程序的终止,则C实现是错误的,因为写入文件(stdout是文件)的数据与抽象机器写入的数据不同。 (这个读数归功于Hans Boehm;我未能将这种微妙之处取出标准。)

     

所以你有它:编译器供应商正在以一种方式阅读标准,而其他人(像我一样)则以另一种方式阅读它。很明显标准是有缺陷的:它应该像C ++或Java一样明确是否允许这种行为。

由于似乎对第二个条件有两个合理但相互矛盾的解释,标准是有缺陷的,应明确定义是否允许这种行为。

答案 6 :(得分:1)

主内存的堆栈部分不是无限的,因此如果以递归方式无限次地调用函数,堆栈将填充有关每个单个函数调用的信息。当没有更多空间用于任何其他函数调用时,这会导致Stack Overflow

答案 7 :(得分:1)

理解C中调用函数堆栈的方式非常重要:

function stack

答案 8 :(得分:1)

C中是否允许无限递归 ?简单的答案是肯定的。编译器将允许您无限地调用函数,直到用完堆栈空间为止;它不会阻止你这样做。

无限递归是否可能?不。如前所述,每次调用函数都需要在程序堆栈上推送返回地址,以及函数运行所需的任何参数。您的程序只有有限的堆栈大小,一旦您用完堆栈,您的应用程序将失败。

假无限递归是否可能?是。可以设计一个自己调用1000次然后允许自己退出1000次函数调用的函数,这样堆栈只能在堆栈上调用原始函数...然后重复整个过程。无限循环。我不认为这是真正的无限递归。

答案 9 :(得分:1)

C中允许无限递归。在编译时,编译器将允许这样做,但是这样做可能会出现运行时错误。

答案 10 :(得分:1)

这是允许的,因为标准说 - &gt;

  

应允许递归函数调用,直接或间接通过任何链   其他功能。

在6.5.2.2中 - &gt; 11

并且Stackoverflow只是简单地发生,因为调用范围的每个状态都必须存储,所以如果必须存储无限的范围状态,那么当你没有无限的内存空间时你的内存就会耗尽。这是定义的行为,因为它发生在运行时,如果递归被破坏,编译器不需要检查标准。

答案 11 :(得分:0)

原因堆栈是有限的,无论何时调用一个函数,它都会保存被调用者(通过将基指针推入堆栈并将当前堆栈指针复制为新的基本指针值)因此消耗堆栈将溢出无限次调用。请参阅调用约定以及堆栈在此处的反应(http://www.csee.umbc.edu/~chang/cs313.s02/stack.shtml

答案 12 :(得分:0)

我只是看了复制最近的draft c standards doc并且没有一个递归引用谈论无限递归。

如果标准文档不要求编译器支持某些内容并且不禁止它,那么编译器开发人员将考虑这种未定义的行为。

答案 13 :(得分:0)

==2907== Process terminating with default action of signal 11 (SIGSEGV)
==2907==  Access not within mapped region at address 0x7FE801FF0

这意味着堆栈由于递归调用和rsp的增量而增长得如此之快,以至于它已访问了该进程为线程堆栈映射的虚拟内存之外的内存地址。页面错误处理程序将注意到该进程尚未分配该虚拟内存区域,并将通过调用内核异常和堆栈展开程序将异常传播到程序。在Windows上,然后将从kernel32.dll .pdata节调用顶级SEH处理程序,该节将调用UnhandledExceptionFilter,可以使用SetUnhandledExceptionFilter从应用程序代码中进行设置。应用程序指定的例程可以调用GetExceptionCode来切换异常常量并显示相关的模态错误对话框。

答案 14 :(得分:0)

#include<iostream>
using namespace std;

int a();
int b();


int a()
{
    cout<<"Hello World\n";
    b();
    return 0;
}
int b()
{
    cout<<"Hello World\n";
    a();
    return 0;
}
void print(int b)
{
   cout << b << endl;
}
int main()
{
   int b = a();
   print(b);
   return 0;
}

此代码在DevC ++中返回此输出:

进程在14.04秒后退出,返回值3221225725

您可以在此处检查此值Dev C++ Process exited with return value 3221225725的含义

因此,C ++中的函数调用将绑定调用堆栈中的每个函数。还知道,这是有限的大小,每个函数调用都会增加此大小。