C中的无限递归

给定具有无限递归的C程序:

int main() {

    main();

    return 0;
}

为什么会导致堆栈溢出。 我知道这导致在C ++中的未定义行为从以下线程是这个无限递归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这是否会导致堆栈溢出? (已被充分回答)


无论何时调用函数,都会将参数压入堆栈,这意味着堆栈段上的数据是“分配的”。 当函数被调用时,返回地址也被CPU压入堆栈,所以它知道返回到哪里。

在你的示例中,这意味着不使用任何参数,所以推送的唯一东西是返回地址,它非常小(x86-32架构上的4个字节),并且另外调整了堆栈帧,这需要另外四个字节在这个架构上。

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

所以这个代码(我把它改名为main()的infinite_recursion()不能被调用)...

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 

UPDATE

关于用于定义递归的标准C99,迄今为止我发现的最好的是在第6.5.2.2节第11段中:

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

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


程序无限递归是不可判定的。 没有明智的标准需要一个即使对于一致性程序也不可能验证的属性,所以没有任何C标准,当前或未来,对于无限递归会有什么话要说(就像没有C标准最终需要一致的程序一样)停)。


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

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_#()基本上都是相同的,但每个iteration_#()都有它自己的x ,并且每个iteration_#()都记得哪个函数调用了它,所以当它调用的函数完成时,它可以正确地返回给调用者。 当程序转换为递归版本时,这个概念不会改变:

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

如果停止条件(从函数returnif检查)被移除,迭代将变为无限。 递归没有返回。 因此,为每个连续的函数调用(本地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

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

然而,你的程序不是tail call优化的候选者,因为递归调用并不是你函数的最后一件事。 你的函数在递归调用之后仍然有一个return语句。 由于在递归调用返回后仍有需要执行的代码,因此优化器无法消除递归调用的开销。 它必须允许调用正常返回,以便后面的代码可以执行。 所以,你的程序将永远支付存储调用代码的返回地址的惩罚。

该标准没有以任何特定的术语来说明“无限递归”。 我收集了我认为与您的问题相关的内容。

  • 递归调用函数是允许的(C.11§6.5.2.2¶¶11)
  • 递归函数调用应允许直接和间接通过任何其他函数链。

  • 递归输入语句会创建局部变量的新实例(C.11§6.2.4¶5,6,7)
  • 一个对象的标识符被声明为没有链接,并且没有存储类特定的静态标识符,它具有自动存储期限,就像一些复合文字一样。 ...

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

    对于具有可变长度数组类型的对象,其生命期从对象声明延伸到程序执行离开声明范围。 如果范围是递归输入的,则每次都会创建一个新的对象实例。

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

    链接地址: http://www.djcxy.com/p/80503.html

    上一篇: Infinite recursion in C

    下一篇: Recursion or Iteration?