转载自:https://hypc-pub.github.io/lua-tutorial/chapter06/tail_calls.html
定义
Lua中函数的一个有趣的特征是可以正确的处理尾调用(proper tail recursion)。
尾调用是一种类似在函数结尾的 goto 调用,当函数最后一个动作是调用另外一个函数时,我们称这种调用尾调用。
function f()
return g()
end
g 的调用是尾调用。
例子中 f 调用 g 后不会再做任何事情,这种情况下当被调用函数 g 结束时程序不需要返回到调用者 f; 所以尾调用之后程序不需要在栈中保留关于调用者的任何信息。一些编译器比如 Lua 解释器利用这种特性在处理尾调用时不使用额外的栈, 我们称这种语言支持正确的尾调用。
特点
由于尾调用不需要使用栈空间,那么尾调用递归的层次可以无限制的。 例如下面调用不论 n 为何值不会导致栈溢出。
function foo(n)
if n > 0 then return foo(n - 1) end
end
需要注意的是:必须明确什么是尾调用。
一些调用者函数调用其他函数后也没有做其他的事情但不属于尾调用。比如:
function f(x)
g(x)
return
end
上面这个例子中 f 在调用 g 后,不得不丢弃 g 的返回值,所以不是尾调用,同样的下面几个例子也不是尾调用:
g(x) -- 没有return语句的明确提示
return g(x) + 1 -- 在g()函数返回之后仍需执行一次加一的指令
return x or g(x) -- 如果g()函数返回多个值,该操作会强制要求g()函数只返回一个值
return (g(x)) -- 同上
Lua 中类似 return g(...) 这种格式的调用是尾调用。 但是 g 和 g 的参数都可以是复杂表达式,因为 Lua 会在调用之前计算表达式的值。 例如下面的调用是尾调用:
return x[i].foo(x[j] + a*b, i + j)
如果没有正确的尾调用,每次调用都要创建一个栈,多次调用后可能导致栈溢出。 但正确的尾调用可以无限制的尾调用,因为每次尾调用只是一个 goto 到另外一个函数并不是传统的函数调用。
栈溢出
调用栈(英语:Call stack,港台称“呼叫堆叠”)别称有:执行栈(execution stack)、控制栈(control stack)、运行时栈(run-time stack)与机器栈(machine stack),是计算机科学中存储有关正在执行的子程序的消息的栈。英文有时直接简称“栈”(the stack),但栈中不一定仅存储子程序消息。几乎所有计算机程序都依赖于调用栈,而高级语言一般将调用栈的细节隐藏至后台。
调用栈最经常被用于存放子程序的返回地址。在调用任何子程序时,主程序都必须暂存子程序执行完毕后应该返回到的地址。因此,如果被调用的子程序还要调用其他的子程序,其自身的返回地址就必须存入调用栈,在其自身执行完毕后再行取回。在递归程序中,每一层次递归都必须在调用栈上增加一条地址,因此如果程序出现无限递归(或仅仅是过多的递归层次),调用栈就会产生栈溢出。
堆栈溢出(英语:stack overflow)在计算机科学中是指使用过多的存储器时导致调用堆栈产生的溢出,也是缓冲区溢出中的一种。堆栈溢出的产生是由于过多的函数调用,导致使用的调用堆栈大小超过事先规划的大小,覆盖其他存储器内的资料,一般在递归中产生。堆栈溢出很可能由无限递归(Infinite recursion)产生,但也可能仅仅是过多的堆栈层级。