【问题标题】:A couple of questions on recursive functions in C language关于C语言递归函数的几个问题
【发布时间】:2013-06-04 10:14:40
【问题描述】:

这是一个获取数字的数字总和的函数:

int sumOfDigits(int n)
{
    int sum=0; //line 1
    if(n==0)
        return sum;
    else
    {
        sum=(n%10)+sumOfDigits(n/10); //line 2
        // return sum;  //line 3
    }
}

在编写此代码时,我意识到局部变量的范围对于函数的每个单独递归都是局部的。那么我是否正确地说如果n=11111, 5 sum 变量在每次递归时被创建并推送到堆栈上?如果这是正确的,那么当我可以使用循环在正常功能中执行递归时,使用递归有什么好处,从而只覆盖一个内存位置?如果我使用指针,递归可能会像普通函数一样占用类似的内存。

现在我的第二个问题,即使这个函数每次都给我正确的结果,我看不出递归(除了返回 0 的最后一个)如何在不取消注释第 3 行的情况下返回值。(使用带有 gcc 的 geany )

我是编程新手,如有错误请见谅

【问题讨论】:

  • 如果您不取消注释第 3 行,则会调用未定义的行为。这可能表现为程序在下周二之前假装按预期工作,如果它愿意的话。

标签: c recursion scope


【解决方案1】:

如果 n=11111,每次递归都会创建 5 个 sum 变量并将其压入堆栈,这是否正确?

从概念上讲,编译器可能会将某些形式的递归转换为跳转/循环。例如。进行尾调用优化的编译器可能会转向

void rec(int i)
{
    if (i > 0) {
        printf("Hello, level %d!\n", i);
        rec(i - 1);
    }
}

相当于

void loop(int i)
{
    for (; i > 0; i--)
        printf("Hello, level %d!\n", i);
}

因为递归调用在尾部位置:当调用时,rec 的当前调用除了给它的调用者一个return 之外没有更多的工作要做,所以它可能以及将其堆栈帧重用于下一次递归调用。

如果这是正确的,那么当我可以使用循环在正常功能中执行递归时,使用递归有什么好处,从而只覆盖一个内存位置?如果我使用指针,递归可能会像普通函数一样占用类似的内存。

对于这个问题,递归是非常不合适的,至少在 C 中是这样,因为循环更具可读性。然而,存在递归更容易理解的问题。树结构的算法就是最好的例子。

(虽然每次递归都可以通过带有显式堆栈的循环来模拟,然后堆栈溢出可以更容易地被捕获和处理。)

关于指针的说法我不明白。

如果没有取消注释第 3 行,我看不到递归(除了最后一个返回 0 的递归)如何返回值。

碰巧。该程序表现出未定义的行为,因此它可以做任何事情,甚至返回正确的答案。

【讨论】:

  • 感谢您解释尾调用优化的概念。当我谈到使用指针时,我并不是真的指的是任何特定问题,而是暗示我们可以在假设问题中更改内存位置的值而不会创建不必要的变量,但我猜编译器很可能会优化任何递归实例后变量未使用。
  • @aste123:在“真”(未优化)递归中,每次递归调用仍然需要一个指针:)
【解决方案2】:

我说得对吗,如果 n=11111,则创建 5 个 sum 变量 并在每次递归时压入堆栈?

递归有 5 层深,因此传统上最终将创建 5 个stack frames(但请阅读下文!),每个都将有空间容纳 sum 变量。所以这在精神上基本上是正确的。

如果这是正确的,那么当我使用递归时有什么好处? 可以使用循环在正常功能中完成,因此只覆盖一个 内存位置?

有几个原因,其中包括:

  • 递归地表达算法可能更自然;如果性能可以接受,那么可维护性就很重要
  • 简单的递归解决方案通常不保持状态,这意味着它们可以轻松并行化,这是多核时代的主要优势
  • compiler optimizations 经常否定递归的弊端

我没有看到递归(除了最后一个返回 0) 返回值而不取消注释第 3 行。

注释掉第 3 行是未定义的行为。为什么要这样做?

【讨论】:

  • 实际上在我写代码的时候,我忘记了return语句,但它给出了预期的结果。这就是我想知道的。猜猜是编译器在做一些智能的事情,并且不会一直工作。
  • @aste123:未定义的行为未定义。不幸的是,很多时候它发生结果如预期。不要养成对此感到满意的坏习惯,事情最终会在没有任何警告的情况下改变。
  • 感谢尾递归问题的链接。
【解决方案3】:

是的,参数和局部变量对于每个调用都是局部的,这通常通过创建程序堆栈上设置的每个调用变量的副本来实现。是的,与使用循环的实现相比,这会消耗更多的内存,但前提是问题可以通过循环和恒定的内存使用来解决。考虑遍历一棵树 - 您必须将树元素存储在某个地方 - 无论是在堆栈上还是在其他一些结构中。递归的优点是更容易实现(但并不总是更容易调试)。

如果您在第二个分支中评论 return sum;,则行为未定义 - 任何事情都可能发生,包括预期的行为。这不是你应该做的。

【讨论】:

    猜你喜欢
    • 2020-03-21
    • 2011-06-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-01-15
    • 2011-09-04
    • 1970-01-01
    • 2020-11-12
    相关资源
    最近更新 更多