2011-03-17 71 views
3

我最近想過這種情況下堆棧溢出:在這種情況下,無限遞歸調用應該引發堆棧溢出?

int f() 
{ 
    return f(); 
} 

int main(void) 
{ 
    f(); 
    return 0; 
} 

,這肯定導致程序崩潰。但我的問題是,爲什麼這與一個無限循環不一樣?在返回線路遞歸調用的情況下,編譯器可以意識到不需要保留調用函數的任何信息,因爲到被調用函數的返回點與調用者的返回點相同。 現在,在這種情況下,我同意編譯器需要保持在堆疊使得呼叫功能的信息:

int f() 
{ 
    int x = f(); 
    return x; 
} 

int main(void) 
{ 
    f(); 
    return 0; 
} 

可以肯定,我失去了一些東西,我想如果有人就向我解釋欣賞。問候

回答

5

事實證明,在一些編譯器中,正確的優化標誌,這其實不會造成堆棧溢出!實際上,我試着在g++中編譯你的程序。在默認情況下進行優化時,會導致堆棧溢出,但是在優化級別-O3處,它會進入無限循環。

無限遞歸和無限循環之間存在差異的原因與默認情況下編譯器如何實現這些構造有關。過去,循環使用分支指令來實現,這些指令告訴處理器在程序的不同部分選擇執行。所有這些指令都是將程序計數器跳到其他位置,這只是修改了寄存器的內容。另一方面,函數調用通過向堆棧添加新的激活記錄來實現,以便對參數,返回地址等進行編碼,以便函數返回時知道返回的位置。

也就是說,這不是函數調用或分支必須實現的「途徑」。理論上,你可以通過函數調用和返回來實現循環,儘管沒有編譯器這樣做。類似地,對於尾遞歸函數(就像你的例子),編譯器通常足夠聰明,可以避免所有堆棧操作,並將其轉換爲樸素的分支指令,以避免堆棧設置和拆卸的開銷。

總之,得到不同行爲的原因是基於編譯器如何決定實現代碼。如果它足夠聰明,可以看到它不需要進行昂貴的函數調用設置,那麼它會將調用轉換爲簡單循環指令並永久循環。如果它沒有檢測到這一點,那麼它會回到天真的函數調用機制。

+0

嘿謝謝:D。這是一個非常有幫助和完整的解釋,是的,這幾乎是我所關心的:編譯器在這些情況下有多聰明。 – Juste 2011-03-17 22:12:22

+0

@ Nicolas-很高興幫忙!順便說一句,如果你喜歡這裏的任何答案(即使它不是我的),你應該接受它,以便問題被標記爲已解決。 – templatetypedef 2011-03-17 22:15:12

+0

完成xD。我仍然在學習瀏覽這個網站lol。我非常喜歡所有的答案,你的答案是最完整的,儘管謝謝^^ – Juste 2011-03-17 22:26:12

4

你不會錯過任何東西。使用gcc -O2編譯程序,並且沒有堆棧溢出。

+0

嘿謝謝。大聲笑我想下次我會嘗試我的編譯器的命令行選項。 – Juste 2011-03-17 22:22:56

0

每次調用一個新函數時,仍然需要在堆棧上推動程序計數器。 這是堆棧在每個函數調用時增長的原因之一。

+0

謝謝:D嗯我忘了PC是的,但它就像沒有必要記住這個特殊例子中的所有值。 – Juste 2011-03-17 22:30:37

0

我的猜測是,C編譯器的編寫者可能不覺得非常有必要優化無限遞歸調用空函數的輸出...不需要

+0

大聲笑是的,這個例子幾乎沒用。但我認爲在某些特殊情況下優化這些呼叫可能會派上用場。 – Juste 2011-03-17 22:15:20

0

C到消除尾調用。 (需要執行TCO的方案。)

+0

感謝:D很高興知道。我會進一步調查 – Juste 2011-03-17 22:28:26

0

我認爲它確實是編譯器的問題,以及它是如何優化的。我會想象在這兩種情況下,一個while循環和這個無休止的遞歸調用,所寫的指令機器儘可能明確地將你的願望轉換成指令。如你所知,在while循環中,你只是回到特定的位置並從那裏繼續。隨着遞歸調用,你正在推出一個新的價值堆棧,所以,根據你的問題,我猜這是一個真正的問題,你的編譯器是多麼的聰明......

+0

感謝:D是的,我的擔心確切地說,當我嘗試的編譯器沒有優化這個編譯器時,我感到有點驚訝,我想這是我的錯,因爲沒有使用適當的參數lol。 – Juste 2011-03-17 22:27:56