【问题标题】:Multiple return statements inside recursive function递归函数内的多个返回语句
【发布时间】:2021-08-31 13:40:02
【问题描述】:

我目前正在阅读 Steve Oualline 的“实用 C 编程第 3 版”。

目前我正在阅读关于递归的第 2 章,在这一章中,作者提出了一个练习,读者必须尝试编写一个递归函数来计算一个数字在数组中出现的次数。

请考虑以下代码:

#include <stdio.h>

size_t count(const int [], size_t, int);

enum {ARRAY_LEN = 11};

int main()
{

    const int num_array[ARRAY_LEN] = {1, 2, 3, 4, 5, 5, 6, 7, 3, 9, 0};

    int number = 0;

    puts("Type a number: (0-9)");
    scanf("%d", &number);

    printf("Number of appearances: %lu\n", count(num_array, ARRAY_LEN, number));

    return 0;

}

size_t count(const int n_array[], size_t arr_len, int num)
{
    return arr_len == 0 ? 0 : ( n_array[arr_len - 1] == num ) + count( n_array, arr_len - 1, num );

}

我的问题是:

C 如何知道将哪个return 带回主函数?因为,在这个例子中,我们有 3 个返回,并且它们都在一定时间内它们的条件变为真,所以,C 怎么知道使用 return 0; 它必须退出递归函数而不是继续在内部寻找另一个返回count() 函数?

谢谢。

【问题讨论】:

  • return 只是返回到调用函数的地方,没有魔法。
  • 每个函数调用最终都会执行一次返回。如果函数以递归方式调用自身,那么该调用将在某个时刻返回,控制权将返回给调用者,而调用者将返回。这两个调用不需要执行相同的返回。每个调用都独立于其他调用。
  • 语句按顺序执行。如果有条件地采用任何return,则该函数将在该点停止执行,并且不考虑任何其他return。第一个if(arr_len == 0) return 0; 结束递归,另外两个决定走哪条递归路径。
  • 对于单个帧,导致return 0; 的控制流不涉及对count 的调用,而其他分支则涉及。这使它成为基本案例而不是递归案例。
  • 但是如果最后一个return返回一个0,那count()函数怎么还会返回出现次数呢?最后,只返回最终的return 发送回main 的内容,对吗?在这种情况下将是一个零

标签: c recursion function-definition


【解决方案1】:

这就是递归编程的意义所在。

代码使用它在 if 链中遇到的第一个返回值。如果该返回递归调用“count”,则该返回将被搁置,直到嵌套的“count”调用返回。这种情况一直持续到arr_len == 0 上的所有内容都返回为止。

【讨论】:

  • 那么在这种情况下,C 会创建 3 个临时返回变量吗?第一个值为 0,第二个值为 count 的增量,第三个值为 count( , , arr_len - 1) ?
  • 不,正如 anatolyg 提到的,每次调用函数调用都会获得它自己的“堆栈框架”和它自己的独立变量。递归函数创建任意数量的堆栈帧,直到第一次调用工作到返回,这比仅仅创建额外的变量需要更多的空间。对于简单的程序,内存和时间的使用不是问题。编写可接受的递归背后有一门完整的科学,这是您对它的介绍。
【解决方案2】:

CPU 有一个堆栈。这是一个非常重要的数据结构,它描述了执行上下文。堆栈可能如下所示:

计数(4, {10, 20, 30}, 3) 计数(4, {10, 99, 33, 22}, 4) 计数(4, {10, 99, 33, 22, 44}, 5) 主要(1)

当 CPU 调用一个函数时,它会在顶部添加另一个 堆栈帧(实际上,有些人或调试器将其可视化为上下颠倒,因此它可能位于底部)。

当 CPU 从函数返回时,它会从顶部移除堆栈帧,并从现在位于顶部的执行点继续。

如果你可视化这个堆栈,你会看到每个return 语句没有任何明确的目的地——它是告诉CPU 继续执行的堆栈。如果main 调用count,则返回main,但如果count 调用count,则返回count。但不仅如此 - 它还记得要返回到哪个调用。

堆栈帧包含调用函数时使用的所有参数的副本,以及函数局部变量的副本(它并不总是副本,但最简单和最重要的情况是它确实是副本)。

【讨论】:

  • 谢谢,从堆栈的角度查看代码,真的帮助我理解发生了什么,非常感谢帮助
【解决方案3】:

对于初学者来说,带有多个返回语句的函数定义很糟糕。

此外,函数应该声明为

size_t count( const int [], size_t, int );

函数可以通过以下方式定义

size_t count( const int a[], size_t n, int num )
{
    return n == 0 ? 0 : ( a[n-1] == num ) + count( a, n - 1, num );
}

即函数的返回类型应为size_t。函数的第一个参数应该有限定符const,因为传递给函数的数组在函数内没有改变。

你应该在 main 中写

printf( "Number of appearances: %zu\n", count( num_array, ARRAY_LEN, number ) );

至于您的问题,很明显,函数内的控件将根据 if 语句中使用的条件传递给 return 语句。

如果arr_len 的值等于0,那么控制将被传递给这个if 语句中的return 语句。

if(arr_len == 0)
    return 0;

注意函数的每次递归调用,参数arr_len的值都会递减。

所以当arr_len等于0时,它是函数的基本情况。在这种情况下,函数停止执行。

否则,如果arr_len 不等于0,那么如果数组的最后一个元素等于num,则将下一次递归调用返回的值加上1,并返回总和。否则,如果数组的最后一个元素不等于 num,则返回下一次递归调用函数返回的值。

if(n_array[arr_len - 1] == num)
    return (1 + count(num, n_array, arr_len - 1));
else
    return count(num, n_array, arr_len -1);

【讨论】:

  • 带有多个返回语句的函数定义不好 这是一种观点,不是事实。许多程序员认为多个 return 语句是可以的。
  • @SteveSummit 一种观点,但我同意。多次退货主要用于“早期逃生”。对于:int fnc(void) { if (early_escape_1) return 1; if (early_escape_2) return 2; return do_stuff(); },就个人而言,我使用 do/while/0 代替:int fnc(void) { int ret; do { if (early_escape_1) { ret = 1; break; } if (early_escape_2) { ret = 2; break; } ret = do_stuff(); } while (0); if (debug) printf("fnc: ret=%d\n",ret); return ret; } 它允许 only return 上的 single 断点,就像这里一样,很容易添加调试printf -- YMMV
  • @CraigEstey 我的意思是问题中的具体代码。实际上有一个重复的调用函数的代码。所以这个 if-else 语句可能会使代码的读者感到困惑,因为这个想法很简单:如果 arr_len 等于 0,则以 0 退出,否则再次调用该函数。但是在函数中有两个 if 语句和一个 else。
  • 感谢@VladfromMoscow,作为初学者,我没有注意到一些重复的代码或对数据类型(int 和 size_t)的误解,但您的回答确实有帮助
【解决方案4】:

当你有递归函数调用时,你有一个函数的多个“实例”处于活动状态,相互调用和返回。为了形象化这一点,我发现想象一下,不是在计算机上运行 C 程序,而是在一个房间里有一群人,你们都给他们一套相同的指令,然后你让他们开始,这很有用为您解决问题。对于您问题中的代码,它会是这样的。

你选择一个人说:“嗨,John,这是一个大小为 3 的数组:[1, 2, 3]。你能告诉我2 出现了多少次吗?”

约翰从你手中接过阵列,他看着他的指示。数组的大小不是0,所以他不会告诉你0。他数组的最后一个元素不是2,所以他也没有做第二件事。但是要做第三件事,他需要进行递归调用,所以他在房间里挑选了其他人,然后说:“嘿,玛丽,这是一个大小为 2 的数组:[1, 2]。你能告诉我多少次吗?数字2出现在里面?”

Mary 从 John 手中接过阵列,并查看了她的指示。数组的大小不是 0,所以她没有告诉 John 0。但是她数组的最后一个元素 2,所以她确实做了第二件事。但要做第二件事,她还需要进行递归调用,所以她选择房间里的其他人说:“嘿,Fred,这是一个大小为 1 的数组:[1]。你能告诉我有多少数字2 出现在其中的次数?”

弗雷德看着他的指示。数组的大小不是0,所以他没有告诉Mary 0。他数组的最后一个元素不是2,所以他没有做第二件事。他也需要进行递归调用来做第三件事,所以他选择房间里的其他人说:“嘿,简,这是一个大小为 0 的数组:[]。你能告诉我多少次吗?数字2出现在里面?”

Jane 看着她的数组,大小 0。所以她说,“Fred,2 在那个数组中出现的次数是:0”。

这正是 Fred 所期待的。 Fred 正在编写第三个return 声明,所以他从简那里听到的一切都是他的答案。他说:“玛丽,2 在那个数组中出现的次数是:0”。

这就是玛丽所期待的。但她正在处理第二个return 声明,因此她在从 Fred 那里听到的任何信息中都加了 1。她说,“约翰,2 在该数组中出现的次数是:1”。

这就是约翰所等待的。约翰正在处理第三个return 声明,所以无论他从玛丽那里听到什么,这也是他的答案。他对你说,“2 在那个数组中出现的次数是:1”。

现在你有答案了!

我曾经教过一堂 C 编程课,有一天我给班上的每个人发了一份讲义,里面有一些关于如何递归、按顺序遍历二叉树的人类可读说明。然后我把一棵“二叉树”——一块纸板,上面有一堆黄色的便签,呈树状排列——递给前排的一个随机学生,开始遍历。这是绝对的混乱,但也很有趣。 (我认为。认为这很有趣,无论如何!)

【讨论】:

  • 感谢史蒂夫,这个答案帮助我从更易于阅读的上下文中理解正在发生的事情。那个使用stickies教授递归二叉树如何工作的故事听起来很神奇哈哈,这是一种非常好的教学方式
猜你喜欢
  • 1970-01-01
  • 2015-12-22
  • 2010-10-30
  • 1970-01-01
  • 2019-10-07
  • 1970-01-01
  • 2012-10-16
  • 2018-06-28
  • 2015-03-26
相关资源
最近更新 更多