【问题标题】:Exit the entire recursion stack退出整个递归栈
【发布时间】:2014-11-10 06:06:12
【问题描述】:

我正在从 main() 调用一个函数 fooA,该函数调用另一个递归函数 fooB。 当我想返回时,我继续使用 exit(1) 来停止执行。当递归树很深时,正确的退出方式是什么?

通过递归堆栈返回可能没有帮助,因为返回通常会清除我构建的部分解决方案,而我不想这样做。我想从 main() 执行更多代码。

我看了Exceptions can be used,如果我能得到一个代码sn-p就好了。

【问题讨论】:

  • 发布您的代码。如果可能,请使用returns 而不是异常。
  • 返回一个表示“立即结束一切”的特定值?例如,返回 -1 并添加逻辑以检查返回的值是否为 -1。如果是这种情况,请返回 -1。
  • 您可以尝试使用 goto。虽然这不是一个很好的解决方案。
  • 代码运行了 1000 行。我会在几秒钟内发布一个简短的版本。 @AlexandreP.Levasseur:看起来不错,我试试看。
  • @SidR 你需要一个longjmp()goto 不会让你出局。但我认为longjmp() 在 C++ 中存在问题。

标签: c++ recursion


【解决方案1】:

goto 语句无法从一个函数跳回另一个函数; Nikos C. 是正确的,它不会考虑释放您所做的每个调用的堆栈帧,因此当您到达您转到的函数时,堆栈指针将指向的堆栈帧你刚才的功能......不,那是行不通的。同样,您不能简单地调用(直接或通过函数指针间接地)您想要在算法完成时结束的函数。在深入研究递归算法之前,您永远不会回到您所处的环境。您可以通过这种方式构建系统,但本质上,每次这样做都会“泄漏”当前堆栈中的内容(与泄漏堆内存不太一样,但效果相似)。如果你深入研究一个高度递归的算法,那可能会出现大量“泄露”的堆栈空间。

不,您需要以某种方式返回到调用上下文。在 C++ 中只有三种方法可以做到这一点:

  1. 依次退出每个函数,将其返回给调用者 以有序的方式通过调用链进行备份。
  2. 抛出异常并在您之后立即捕获它 启动到您的递归算法(自动销毁 堆栈上的每个函数按顺序创建的任何对象 时尚)。
  3. 使用 setjmp() 和 longjmp() 来做类似投掷 & 捕获异常,但“抛出” longjmp() 不会破坏 堆栈上的对象;如果任何此类对象拥有堆分配, 这些分配将被泄露。

要执行选项 1,您必须编写递归函数,以便在达到解决方案后,它会向其调用者返回某种指示它已完成(可能是同一个函数),并且其调用者会看到这一事实& 通过返回它(可能是同一个函数)将这个事实传递给它的调用者,依此类推,直到最后递归算法的所有堆栈帧都被释放,然后你返回到调用第一个函数的任何函数递归算法。

要执行选项 2,您将对递归算法的调用包装在 try{...} 中,然后紧随其后 catch(){...} 预期的抛出对象(可以想象,这可能是计算的结果,或者只是一些让来电者知道“嘿,我已经完成了,你知道在哪里可以找到结果”)。示例:

try
{
    callMyRecursiveFunction(someArg);
}
catch( whateverTypeYouWantToThrow& result )
{
    ...do whatever you want to do with the result,
    including copy it to somewhere else...
}

...在您的递归函数中,当您完成结果时,您只需:

throw(whateverTypeYouWantToThrow(anyArgsItsConstructorNeeds));

要做选项 3...

#include <setjmp.h>
static jmp_buf jmp; // could be allocated other ways; the longjmp() user just needs to have access to it.
    .
    .
    .
if (!setjmp(jmp)) // setjmp() returns zero 1st time, or whatever int value you send back to it with longjmp()
{
    callMyRecursiveFunction(someArg);
}

...在您的递归函数中,当您完成结果时,您只需:

longjmp(jmp, 1); // this passes 1 back to the setjmp().  If your result is an int, you
                 // could pass that back to setjmp(), but you can't pass zero back.

使用 setjmp()/longjmp() 的坏处是,如果调用 longjmp() 时堆栈上仍有任何堆栈分配的对象仍然“活动”,则执行将跳回 setjmp() 点,跳过这些对象的析构函数。如果您的算法仅使用 POD 类型,那不是问题。如果您的算法使用的非 POD 类型不包含任何堆分配(例如来自 malloc()new),这也不是问题。如果您的算法使用包含堆分配的非 POD 类型,那么您只有使用上面的选项 1 和 2 才是安全的。但是,如果您的算法符合 setjmp()/longjmp() 的标准,并且如果您的算法在完成时被大量递归调用所掩盖,那么 setjmp()/longjmp() 可能是最快的返回方式到初始调用上下文。如果这不起作用,就速度而言,选项 1 可能是您最好的选择。选项 2 可能看起来很方便(并且可能会在每次递归调用开始时消除条件检查),但与系统自动展开调用堆栈相关的开销有些显着。

通常说您应该为“异常事件”(预计非常罕见的事件)保留异常,而与展开调用堆栈相关的开销就是原因。较早的编译器使用类似于 setjmp()/longjmp() 的东西来实现异常(setjmp() 在trycatch 的位置,longjmp() 在throw 的位置),但是有当然,与确定堆栈上的哪些对象需要销毁相关的额外开销,即使没有这样的对象也是如此。另外,每次遇到try 时,它都必须保存上下文以防有一个throw,如果异常是真正的异常事件,那么节省的时间这种背景完全被浪费了。较新的编译器现在更有可能使用所谓的“零成本异常”(a.k.a. 基于表的异常),这似乎可以解决世界上所有的问题,但它并没有......它使正常运行时更快,因为不再需要在每次遇到try 时保存上下文,但如果执行throw,现在更多开销与存储在运行时必须处理的大量表,以便根据遇到throw 的位置和运行时堆栈的内容来确定如何展开堆栈。所以例外不是免费的,尽管它们非常方便。你会在互联网上找到很多东西,人们声称它们的价格是多么的不合理,它们会减慢你的代码速度,你还会发现很多人驳斥这些说法,双方都提出了艰难的看法数据来支持他们的主张。您应该从参数中得到的好处是,如果您希望它们很少发生,那么使用异常是很好的,因为它们会产生更清晰的接口和逻辑,而无需在每次进行函数调用时进行大量条件检查“坏”。但是你不应该使用异常作为调用者和它的被调用者之间正常通信的手段,因为这种通信模式比简单地使用返回值要昂贵得多。

【讨论】:

    【解决方案2】:

    这发生在我找到从根到二叉树节点的路径时。我正在使用堆栈来按顺序存储节点,并且递归不会停止,直到最后一个节点返回 NULL。我使用了一个全局变量,整数 i=1,当我到达我正在寻找的节点时,我将该变量设置为 0 并使用 while(i==0) 返回堆栈;允许程序在不弹出我的节点的情况下备份内存堆栈。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2015-04-04
      • 2012-07-26
      • 2017-01-20
      • 2018-12-02
      • 2017-09-06
      • 2019-07-08
      • 2015-08-05
      相关资源
      最近更新 更多