【问题标题】:How Recursion works in C递归在 C 中的工作原理
【发布时间】:2011-08-03 15:19:48
【问题描述】:

我是 C 的新手,我正在阅读有关递归的内容,但我完全糊涂了。

我感到困惑的主要部分是当达到退出条件时事情会如何放松。我想知道在递归期间值是如何从堆栈中推送和弹出的。

还有谁能给我一个递归的图解视图?

谢谢...

【问题讨论】:

  • 了解事物的运作方式……科学、工程、系统……都需要一种双重思考。你假装你只知道一些理想化环境中的一小部分问题。非常强大的聚焦方式。递归是一种终极形式。只看proc的内部,忘记外部。每一位都按照它的指示去做,并且总的来说会发生一些有用的事情。
  • 您是否对递归的一般工作方式感到困惑,或者对在 c 中发生递归时汇编代码级别发生的事情感到困惑?您是指堆栈的图表视图吗?
  • 递归调用实际上与调用其他例程没有什么不同。
  • 你了解堆栈的工作原理吗?基本上每次函数调用自身时,它都会添加到堆栈中。当函数返回时,它会从堆栈中弹出。
  • 老实说,我对递归的一般工作方式感到困惑,但当我试图了解当基本条件达到时事情如何展开时,它更加令人困惑。实际上我想知道递归第一个函数调用何时发生然后值如何推送到堆栈上以及基本条件何时达到它的弹出方式......以及return语句如何在那里工作???

标签: c recursion


【解决方案1】:

让我们假设一个函数:

int MyFunc(int counter) {
    // check this functions counter value from the stack (most recent push)

    // if counter is 0, we've reached the terminating condition, return it
    if(counter == 0) {
        return counter;
    }
    else {
        // terminating condition not reached, push (counter-1) onto stack and recurse
        int valueToPrint = MyFunc(counter - 1);

        // print out the value returned by the recursive call 
        printf("%d", valueToPrint);

        // return the value that was supplied to use 
        // (usually done via a register I think)
        return counter;
    }
}

int main() {
    // Push 9 onto the stack, we don't care about the return value...
    MyFunc(9);
}

输出为:012345678

第一次通过MyFunc,count为9。终止检查失败(不是0),所以调用递归调用,(counter -1),8。

这样重复,每次递减压入堆栈的值,直到counter == 0。此时,终止子句触发,函数简单地返回 counter (0) 的值,通常在寄存器中。

下一次调用堆栈,使用返回的值打印 (0),然后返回调用时提供给它的值 (1)。重复:

下一次调用堆栈,使用返回的值打印 (1),然后返回调用时提供给它的值 (2)。等等,直到你到达堆栈的顶部。

所以,如果 MyFunc 被 3 调用,你会得到(忽略堆栈中的返回地址等)的等价物:

Call MyFunc(3) Stack: [3]
Call MyFunc(2) Stack: [2,3]
Call MyFunc(1) Stack: [1,2,3]
Call MyFunc(0) Stack: [0,1,2,3]
Termination fires (top of stack == 0), return top of stack(0).
// Flow returns to:
MyFunc(1) Stack: [1,2,3]
Print returned value (0)
return current top of stack (1)

// Flow returns to:
MyFunc(2) Stack: [2,3]
Print returned value (1)
return current top of stack (2)

// Flow returns to:
MyFunc(3) Stack: [3]
Print returned value (2)
return current top of stack (3)

// and you're done...

【讨论】:

  • @Thanks Forsvarir 为你的解释......我想知道的一件事是只有一个返回语句整个堆栈被弹出???
  • @AMIT:return 语句从当前正在执行的函数实例返回......然后前一个实例继续执行直到它到达 return 语句
【解决方案2】:

在 C 中,递归就像普通的函数调用一样。

  1. 调用函数时,参数、返回地址和帧指针(我忘记了顺序)被压入堆栈。
  2. 在被调用函数中,首先将局部变量的空间“压入”堆栈。
  3. 如果函数返回一些东西,把它放在某个寄存器中(取决于架构,AFAIK)
  4. 撤消步骤 2。
  5. 撤消步骤 1。

因此,使用递归步骤 1 和 2 执行几次,然后可能执行 3(可能只执行一次),最后执行 4 和 5(与 1 和 2 一样多次)。

【讨论】:

  • 3 要么不被执行(函数不返回值),要么经常执行 1/2 等等......如果你进入一个递归函数并返回一些东西,它会每次都这样做
  • @forsvarir:对,如果函数不是尾递归的,那么 3 将不止一次执行。如果它是尾递归的,那么编译器可以生成只发生一次的代码。
  • gotcha :) 如果你知道它已经包含你要放在那里的值,那么加载寄存器值没有意义。
【解决方案3】:

另一种答案是您通常不知道。 C 作为一种语言没有任何堆堆栈。您的编译器使用称为堆栈的内存位置来存储控制流信息,例如堆栈帧、返回地址和寄存器,但 C 语言中没有任何内容禁止编译器将这些信息存储在其他地方。对于实际方面,前面的答案是正确的。这就是当今 C 编译器的运作方式。

【讨论】:

【解决方案4】:

当达到退出条件时,事情如何放松?

首先,关于递归的几句话:divide and conquer method 用于复杂任务,可以逐渐分解并简化为初始任务的简单实例,直到形成形式基本案例 允许直接计算。这是一个与mathematical induction密切相关的概念。

更具体地说,递归函数直接或间接调用自身。在直接递归函数中,foo() 再次调用自身。在间接递归中,函数foo() 调用函数moo(),后者又调用函数foo(),直到达到基本情况,然后,最终结果以与初始递归完全相反的顺序累加函数调用。

示例:

阶乘 n,表示为 n!,是从 1n 的正整数的乘积。阶乘可以正式定义为:
factorial(0)=1, (base case)
factorial(n)= n * factorial (n-1),对于 n > 0。 (递归调用

递归出现在这个定义中,因为我们根据 factorial(n-1) 定义 factorial(n)

每个递归函数都应该有终止条件来结束递归。在本例中,当 n=0 时,递归停止。上面用C表示的函数是:

int fact(int n){
    if(n == 0){ 
        return 1;
    }
    return (n * fact(n-1));
}

这个例子是一个直接递归的例子。

这是如何实现的? 在软件层面,它的实现与实现其他功能(过程)没有区别。一旦您了解每个过程调用实例都与其他实例不同,递归函数调用自身这一事实并不会产生任何重大影响。

每个活动过程都维护一个activation record,它存储在堆栈中。激活记录由参数(调用者的)返回地址局部变量组成。

激活记录在过程被调用时存在,在过程终止并将结果返回给调用者后消失。因此,对于每个未终止的过程,都会存储一个包含该过程状态的激活记录。激活记录的数量以及因此运行程序所需的堆栈空间量取决于递归的深度。

还有谁能给我一个递归的图解视图吗?

下图显示factorial(3)的激活记录:

从图中可以看出,对阶乘的每次调用都会创建一个激活记录,直到达到基本情况,并从那里开始以乘积的形式累积结果。


【讨论】:

    【解决方案5】:

    这个问题已得到广泛回答。请允许我使用更具教学性的方法加入一个额外的答案。

    您可以将函数递归视为具有两个不同阶段的气泡堆栈:推动阶段和爆发阶段。


    A) PUSHING STAGE(或“推栈”,正如 OP 所说)

    0) 起始 Bubble #0 是 MAIN 函数。它被这些信息炸毁了:

    • 局部变量。
    • 对下一个 Bubble #1 的调用(第一次调用递归 函数,MYFUNC)。

    1) Bubble #1 在轮到它时被以下信息炸毁:

    • 上一个 Bubble #0 中的参数。
    • 必要时使用局部变量。
    • 返回地址。
    • 使用返回值终止检查(例如:if (counter == 0) {return 1})。
    • 调用下一个 Bubble #2。

    请记住,这个气泡和其他气泡一样,是递归函数 MYFUNC。

    2) Bubble #2 使用与 Bubble #1 相同的信息被炸毁,从后者获取必要的输入(参数)。在此之后,您可以根据需要堆叠任意数量的气泡,根据气泡 #1 中列出的项目相应地膨胀信息。

    i) 因此,您可以得到任意数量的气泡:Bubble #3、Bubble #4...、Bubble #i。最后一个气泡在终止检查中有一个 NAIL。请注意!

    B) BURSTING STAGE(或“弹出堆栈”,正如 OP 所说)

    这个阶段发生在您达到肯定的终止检查并且包含钉子的最后一个气泡破裂时。

    假设这个阶段发生在 Bubble #3 中。达到肯定的终止检查,并且 Bubble #3 破裂。然后从这个泡沫中解放出来的钉子。这颗钉子落在 Bubble #2 下方并将其爆裂。发生这种情况后,钉子会随着它的坠落而破裂,直到它爆破 Bubble #1。 Bubble #0 也是如此。重要的是要注意,钉子会跟随泡沫中的返回地址,此时它正在破裂:地址告诉钉子在坠落时要遵循的方向。

    在这个过程结束时,得到答案并将其传递给 MAIN 函数(或 Bubble #0,当然不是爆裂)。


    C) 以图形方式(按照 OP 的要求)

    这是图形解释。它从底部,气泡 #0 到顶部,气泡 #3。

    /*Bubble #3 (MYFUNC recursive function): Parameters from Bubble #2,
    local variables, returning address, terminating check (NAIL),
    call (not used here, as terminating check is positive).*/
    

    向上推到上面的气泡 ↑ --------------------------------- -------------------- ? 钉子掉到泡泡#2

    /*Bubble #2 (MYFUNC recursive function): Parameters from Bubble #1,
    local variables, returning address, terminating check (not used),
    call to Bubble #3.*/
    

    向上推到上面的气泡 ↑ --------------------------------- -------------------- ? 钉子掉到泡泡#1

    /*Bubble #1 (MYFUNC recursive function): Parameters from Bubble #0,
    local variables, returning address, terminating check (not used),
    call to Bubble #2.*/
    

    向上推到上面的气泡 ↑ --------------------------------- -------------------- ? 钉子掉到泡泡#0

    /*Bubble #0 (MAIN function): local variables, the first call to Bubble #1.*/
    

    希望这种方法对某人有所帮助。如果需要任何澄清,请告诉我。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-06-15
      • 2014-10-29
      • 1970-01-01
      • 2019-06-07
      • 1970-01-01
      • 2012-09-02
      • 1970-01-01
      • 2022-01-25
      相关资源
      最近更新 更多