如果您想避免无限递归的堆栈溢出,不幸的是,您将不得不深入研究一些程序集以更改堆栈,以便新的激活记录不会不断地推入堆栈,在某些时候会导致溢出。因为您在函数的末尾进行递归调用,所以在递归流行的其他语言(即,Lisp、Scheme、Haskell 等)中调用它是一种跟踪调用优化。它通过基本上将尾调用转换为循环来防止堆栈溢出。在 C 中会是这样的(注意:我在 x86 上使用带有 gcc 的内联汇编,并且我将您的参数更改为int
fromdouble
为了简化组装。此外,我已从 C++ 更改为 C,以避免函数名的名称混乱。最后,每个语句末尾的 "\n\t" 不是实际的汇编命令,而是 gcc 中的内联汇编所必需的):
#include <stdio.h>
void rek(int x)
{
printf("Value for x: %d\n", x);
//we now duplicate the equvalent of `rek(x+1);` with tail-call optimization
__asm("movl 8(%ebp), %eax\n\t" //get the value of x off the stack
"incl %eax\n\t" //add 1 to the value of x
"movl 4(%ebp), %ecx\n\t" //save the return address on the stack
"movl (%ebp), %edx\n\t" //save the caller's activation record base pointer
"addl $12, %ebp\n\t" //erase the activation record
"movl %ebp, %esp\n\t" //reset the stack pointer
"pushl %eax\n\t" //push the new value of x on the stack for function call
"pushl %ecx\n\t" //push the return value back to the caller (i.e., main()) on the stack
"movl %edx, %ebp\n\t" //restore the old value of the caller's stack base pointer
"jmp rek\n\t"); //jump to the start of rek()
}
int main()
{
rek(1);
printf("Finished call\n"); //<== we never get here
return 0;
}
在 Ubuntu 10.04 上使用 gcc 4.4.3 编译,它在无限循环中几乎“永远”运行,没有堆栈溢出,因为没有尾调用优化,它很快就因分段错误而崩溃。您可以从该__asm
部分的注释中看到堆栈激活记录空间是如何被“回收”的,因此每个新调用都不会用完堆栈上的空间。这包括将键值保存在旧的激活记录(前一个调用者的激活记录基指针和返回地址)中,并恢复它们,但为下一次递归调用函数而更改了参数。
同样,其他语言,主要是函数式语言,将尾调用优化作为该语言的基本特征。因此,Scheme/Lisp/等中的尾调用递归函数。不会溢出堆栈,因为当新函数调用作为现有函数的最后一条语句时,这种类型的堆栈操作是在后台为您完成的。