为什么像C,Pascal这样的语言不能实现尾递归?

时间:2014-12-22 03:34:48

标签: c scheme pascal tail-recursion sicp

我正在阅读SICP。其中一个footnote提到:

  

使编译器生成尾递归代码似乎是一个简单的想法。但是大多数常用语言编译器(包括C和Pascal)都没有这样做,因此这些语言不能仅仅根据过程调用来表示迭代过程。这些语言中尾递归的难点在于它们的实现使用堆栈来存储过程参数和局部变量以及返回地址。


如果将堆栈用于过程参数,局部变量和返回地址,我无法理解为什么不可能实现尾递归。

2 个答案:

答案 0 :(得分:10)

C当然可以实现尾递归。 C中的尾递归看起来像这样:

int foo (int bar)
{
    int baz;
    ...
    return foo(baz);
}

所有C编译器都可以做到这一点。其中一些(实际上大多数)为它提供优化,因此它不使用额外的堆栈空间,如此(gcc,MSVC和clang / LLVM):

我对Pascal知之甚少,但这里是对2004年支持尾递归的非基于LLVM的pascal编译器的引用:

鉴于LLVM案例适用于多种语言并且可能是最常见的现代编译器后端,并且鉴于这些是最常见的C编译器,并且鉴于您的源代码似乎不区分尾递归和尾递归而不使用堆栈空间,我建议您的来源不对或最多过时。

问题的第二部分:

  

如果将堆栈用于过程参数,局部变量和返回地址,我无法理解为什么不能实现尾递归。

可以使用堆栈实现尾递归,就像任何其他递归一样。但是,如果你没有优化它,所以使用堆栈(例如上面的链接),那么深度递归将导致你的堆栈空间不足。可以说,在内存便宜且堆栈大小不受32位内存映射约束的现代环境中,这不是一个问题。但是,鉴于大多数编译器优化并且无论如何都可以避免堆栈,它也适用于其他更具挑战性的环境。

答案 1 :(得分:7)

  

这些语言中尾递归的难点在于它们的实现使用堆栈来存储过程参数和局部变量以及返回地址。

在机器语言中,没有函数调用,因此函数调用被转换为

  1. 将参数推送到调用堆栈
  2. 将返回地址推入堆栈
  3. 转到要调用的子程序的正文
  4. 当子程序退出时,它会返回到我们之前推送到堆栈的返回地址
  5. 参数从堆栈中删除
  6. 现在这个“调用约定”模式有两个基本变体,与谁负责第5步(从堆栈中删除函数参数)有关。在C中,调用约定是函数调用者负责清理堆栈。这允许像printf这样的可变函数(在这些情况下,只有调用者知道在调用完成后弹出的正确参数数量),但意味着你不能进行尾调用优化,因为“返回”不是最后一个函数做的事情(之后你还需要清理堆栈)。另一方面,如果您的调用约定是让函数本身清理堆栈,那么您将失去具有可变参数函数但能够具有TCO的能力。