尾调用(Tail Call)是指在调用函数时直接将被调函数的返回值作为调用函数的返回值返回。如果这个调用是调用了调用函数本身(递归
调用),这叫做尾递归(Tail Recursive)。使用尾调用的一个优点在于它不消耗额外的调用栈空间,它可以替换当前的栈帧。因此,使
用尾调用来替换标准的调用被称作尾调用消除(Tail Call Elimination),或者尾调用优化(Tail Call Optimization)。
尾调用的例子:
function foo(data)
A(data);
return B(data);
B(data)就是一个尾调用。
现在来看以下几个例子:
function foo1(data)
return A(data) + 1;
function foo2(data)
var ret = A(data);
return ret;
function foo3(data)
var ret = A(data);
return (ret == 0) ? 1 : ret;
这里,只有foo2使用了尾调用,foo1和foo3在调用A(data)之后还需要将主控权交回调用函数,因此都不是尾调用。
关于尾递归
关于递归操作,大家都应该不陌生。例如,我们可以使用递归来计算一个单向链表的长度:
int GetLenRecursively(Node *head)
{
if (head == NULL)
return 0;
return GetLenRecursively(head->next) + 1;
}
如果单链表的十分长,那么上面这个方法可能会遇到栈溢出,这是由于每个线程在执行代码时都会分配一定尺寸的栈空间(Windows系统
中为1M还是2M),每次函数调用时都会在栈里存放一些信息(如参数、返回地址、局部变量等),这样如果调用次数过多,自然就会超过
线程的栈空间了。因此,我们可以将递归改为如下形式:
int GetLenTailRecursively(Node *head, int acc)
{
if (head == NULL)
return acc;
return GetLenTailRecursively(head->next, acc + 1);
}
GetLenTailRecursively函数多了一个acc参数,acc的为accumulator(累加器)的缩写,它的功能是在递归调用时“积累”之前调用的结果,并将其传入下一次递归调用中——这就是GetLenTailRecursively函数与GetLenRecursively函数相比在递归方式上最大的区别:GetLenRecursively函数在递归调用后还需要进行一次“+1”,而GetLenTailRecursively的递归调用属于函数的最后一个操作。这就是所谓的“尾递归”。与普通递归相比,由于尾递归的调用处于函数的最后,因此函数之前所积累下的各种状态对于递归调用结果已经没有任何意义,因此完全可以把本次函数中留在堆栈中的数据完全清除,把空间让给最后的递归调用。这样的优化便使得递归不会在调用堆栈上产生堆积,意味着即时是“无限”递归也不会让堆栈溢出。这便是尾递归的优势。