我最近考虑过这种堆栈溢出的情况:
int f()
{
return f();
}
int main(void)
{
f();
return 0;
}
肯定会导致程序崩溃。但我的问题是为什么这与无限循环不一样?在返回行的递归调用的情况下,编译器可以意识到不需要保留调用函数的任何信息,因为被调用函数的返回点与调用者的返回点相同。 现在,在这种情况下,我同意编译器需要保留在堆栈中进行调用的函数的信息:
int f()
{
int x = f();
return x;
}
int main(void)
{
f();
return 0;
}
我肯定错过了什么,如果有人向我解释,我会很感激。此致
答案 0 :(得分:5)
事实证明,在某些编译器中,使用正确的优化标志,实际上不会导致堆栈溢出!事实上,我尝试在g++
中编译您的程序。在默认情况下进行优化时,会导致堆栈溢出,但在优化级别-O3
,它只会进入无限循环。
无限递归和无限循环之间存在差异的原因与默认情况下编译器如何实现这些结构有关。历史上已经使用分支指令实现循环,该分支指令告诉处理器在程序的不同部分获取执行。所有这些指令都是在其他地方跳转程序计数器,这只是修改寄存器的内容。另一方面,函数调用是通过向堆栈添加新的激活记录来实现的,以对参数,返回地址等进行编码,以便在函数返回时知道返回的位置。
也就是说,这不是必须实现函数调用或分支的“方式”。理论上,你可以通过使用函数调用和返回来实现循环,尽管没有编译器这样做。类似地,对于尾递归的函数(就像你的例子一样),编译器通常足够聪明,可以忽略所有堆栈操作并将其转换为简单的分支指令,以避免堆栈设置和拆卸的开销。
简而言之,您获得不同行为的原因取决于编译器如何决定实现代码。如果足够聪明地看到它不需要进行昂贵的函数调用设置,那么它会将调用转换为简单的循环指令并永远循环。如果它没有检测到这一点,那么它将回退到天真的函数调用机制。
答案 1 :(得分:4)
你没有遗漏任何东西。使用gcc -O2
编译程序,没有堆栈溢出。
答案 2 :(得分:0)
每次调用新函数时,仍然需要在堆栈上推送程序计数器。 这是堆栈在每次函数调用时增长的原因之一。
答案 3 :(得分:0)
我的猜测是C编译器编写者可能认为没有必要优化对空函数的无限递归调用的输出......
答案 4 :(得分:0)
C不需要消除尾调用。 (计划需要执行TCO。)
答案 5 :(得分:0)
我认为这确实是编译器的问题,以及它是如何优化的。我想象在这两种情况下,一个while循环和这个无休止的递归调用,机器 所写的指示尽可能明确地将您的意愿转化为指示。如你所知,对于while循环,你只是jmp'ing回到特定位置并继续从那里。通过递归调用,你正在推动堆栈上的新值,所以,根据你的问题,我猜它真的是你的编译器有多聪明的问题......