這個問題是知乎上的一個問題,看了以后覺得比較有意思。代碼短到只有十多行,但是這么短的代碼卻輸出了很奇怪的結果。很多人回答的時候都是站在理論的角度上說明代碼的問題,但是實際的問題還是沒有說明其中的問題。
問題是“C 語言局部變量,堆與棧的問題?”
問題的地址如下:https://www.zhihu.com/question/60415017
知乎上的問題
以上就是知乎中的問題,基本上把問題也描述清楚了,對於它的問題看似詭異,其實並不復雜。這個問題涉及幾個知識點,第一是關於內存分配的問題,第二是關於函數調用時棧幀的開辟與回收的問題。當然了,如果是純理論的描述問題,其實只會把問題越搞越糊塗,如果結合調試器問題就不同了。
以下是我在知乎的回答(因為當時回答時隨意了一些,所以這里再簡單的整理了一下,從分割線開始,就是我整理過的回答了)。
遇到類似的問題,通過在調試器中進行單步調試,然后再觀察其反匯編代碼,一般就知道其中的問題所在了。
先來了解幾個簡單的概念性的問題:
首先,局部變量保存在棧中;
其次,new 分配的空間在堆中。
棧空間是由 ESP 和 EBP 尋址(x86架構的平台下),這兩個寄存器是由 CPU 控制維護的。ebp 作為棧幀的基址來說,函數調用完后會自動恢復到被調用之前,那么棧中的數據其實還是存在的。esp 作為棧頂指針,在函數返回后,也會被收回。雖然棧幀在函數返回后被回收,但是其中的數據並沒有被回收,因此之前的數據仍然是存在的。很多書上說,訪問這樣的地址會給出隨機值,其實不是,只是這些值我們不再確定是什么值而已,但是它不是隨機的。
new 出來的堆空間,如果不 delete 是不會釋放的,也就是說 new 完以后的地址只要不釋放,在其他代碼中都可以使用。
以上就是 堆 空間和 棧 空間的簡單描述。
上面是理論部分,下面實際觀察一下。
我用的環境是 VS2012,和提問者的環境不同,但是過程是相同的。
看一下 func 函數的反匯編代碼,這里我用的 DEBUG 方式編譯的。
在 func 函數的 return 處下斷點,然后運行到此處,觀察其反匯編代碼,並打開寄存器窗口、監視窗口和內存窗口。
看下面的截圖:
變量的地址是 0x0103fd6c,而 i 的值是0x0132a670,這值是一個地址,也就是由 new 分配的堆地址,看一下 0x0132a670 這個地址中的值,如下圖:
而 0x0103fd6c 是變量 i 的地址,這個地址在棧中,如下圖:
上面的寄存器的值是在 func 函數中的值,看一下 ebp 和 esp 的值。
返回 main 函數,如下圖:
上圖是返回 main 函數后的寄存器的值。
再看 0x0132a670 地址中內存的值仍然沒變……
這就是堆的效果,即 new 的情況。
這部分內存如果不是人為去寫,一般數據不會被修改或覆蓋。
前面說的是數組在堆中的情況,如果是在棧中的話,那么數組 i 的值都在棧中,即7、9、5 也在棧中。
簡單說一下。
仍然在 func 的 return 處下斷點,運行到這里,觀察:
此時在 func 函數內,繼續單步返回到 main 函數內:
觀察,現在 ESP 和 EBP 已經恢復到 main 函數的棧幀內,而且代碼也運行到了 main 的 for 內。
但是內存的棧中,func 函數內的 i 數組仍然存在。雖然棧幀被回收,但是數據仍在,通常情況是無法訪問它們的,但是現在把 i 的地址返回給 main 函數,因此還是可以訪問到它的。
發現執行到完 call 以后,棧中的數據被破壞了,因為用的是單步步過,其實只要進入 call 以后,原來棧中的數據就被破壞了。
那么為什么 7 能被正確的輸出呢?因為在棧還沒破壞之前,7 已經當作 printf 的參數被送入棧中當作參數了。看那句 push edx 即可。
剩下的輸出就不說了,反正棧已經被破壞了。剩下的就理所當然有問題了。
以上就是我給出問題的答復,其實整個過程還算簡單。記得我在學習的時候,我的老師說過這么一句話,“學編程不看內存,相當於游泳不下水”。當然了,也許並不是每門編程語言都有機會去觀察其運行時的內存情況,但是,了解如何調試還是非常有趣的事情,因為很多看似不好解釋的問題,其實在調試器下面都是可以看到問題本質的。
我的微信公眾號:“碼農UP2U”