酒店房間和C++局部變量的作用域
英文原文:Can a local variable's memory be accessed outside its scope?
問題:Can a local variable’s memory be accessed outside its scope? 有一段局部變量的內存,可以從其范圍之外訪問它么?
如下代碼:
int *foo () { int a = 5; return &a; } int main () { int *p = foo (); cout << *p; *p = 8; cout << *p; }
這樣的代碼可以正常執行,而且沒有任何運行時的異常!
輸出是
5 8
這是怎么回事?難道局部變量在函數外也可以被訪問嗎?
來自微軟資深軟件工程師 Eric Lippert 的最佳答案(3200+ 贊):
你在酒店里租了一間房。你把一本書放進了桌子的第一個抽屜里,然后就去睡覺了。當你第二天早上醒來時,你假裝忘記去還鑰匙了。你偷了房間的鑰匙!
一周之后,你回到了酒店,但沒有入住,你用偷來的鑰匙溜進了你上次入住的房間,并查看了那個抽屜。你的書還在那里。是不是很令人吃驚!
這是怎么回事呢?難道一個酒店房間的抽屜不是應該無法被一個沒有入住這個房間的人看到嗎?
好吧,明顯的是,這種情況在真實世界中當然會發生。在你不入住這個房間的時候,這里面沒有任何神秘的力量把你的書弄消失掉,也沒有魔法能夠阻止你用偷來的鑰匙進入房間。
酒店的管理規章里沒有要求拿走你的書。你也沒有跟他們說如果你落下了一本書,他們可以幫你撕毀它。如果你用偷來的鑰匙非法進入了你上次的房間, 并且沒有被酒店的安保系統發現。你也沒有跟他們說如果你之后嘗試溜進房間,他們應該阻止你。不過事實上,你確實簽了一份協議規定你保證不會偷偷溜回房間。 只不過你打破了協議。
在這種情況下任何事情都有可能發生。如果你運氣好的話,那本書可能還在那里。其他人的書也可能在那個抽屜里而你的書則被丟進了酒店的火爐里。也 可能當你溜進去的時候正好有個人在把你的書撕成碎片。酒店可能把那張桌子連帶你的書都移走了,而把一個衣柜放在那里。這家酒店也可能正好要被拆除,換成一 個足球場,在你溜來溜去的時候。你可能會在一場爆破中死去。
當你離開酒店而偷了房間的鑰匙的時候,你不知道將會發生什么。你放棄了去生活在一個可靠的,安全的世界里,因為你選擇去打破系統的規則。
C++不是一門安全的語言。你可以非常輕松就打破這個系統的規則。如果你嘗試去做一些非法并且愚蠢的事情,比如你回到那個你已經不入住的房間, 并想要去查看那張也許已經不存在的桌子。C++不會阻止你的。比 C++ 更加安全的語言通過限制你的能力來解決這個問題,比如通過更加嚴格的控制房間鑰匙。
【更新】:
我的老天。這個答案獲得了這么多的關注。(我不知道為什么,我只是覺得這樣比喻比較有趣, 不過管他呢。)
我認為在經過了更加技術性的思考之后更新一下這個答案是必要的。
編譯器的工作是生成代碼來管理這個程序數據擁有的內存。有很多方式來生成管理內存的代碼,但是這么多年來有兩個基本的技術是必須要知道的。
第一個是擁有一片長期存在的區域,這片存儲區域里的每一個字節,他們的生命周期比較長。生命周期的意思就是它們能夠被程序訪問的時期。這類內存 沒辦法提前進行預估。編譯器生成一種叫堆管理器的代碼,它知道如何在需要的時候動態的分配內存,當內存不再被需要的時候釋放掉他們。
第二個是擁有一片短期存在的區域,這片存儲區域里的每一個字節都可以提前進行預估。而且比較特殊的是,這片區域的生命周期遵循一種嵌套模式。也就是說,在這片區域中擁有最長生命周期的變量,它所分配的內存地址被它之后分配的那些生命周期較短的變量所重用。
局部變量就是第二種情況。當調用一個函數時,它的局部變量便被生成了。當這個函數調用另外一個函數時,新函數的局部變量也被生成了。這些變量會在第一個函數的局部變量之前被釋放掉。這些局部變量的內存地址的開始和結束可以提前被計算出來。
因為這個原因,局部變量經常被分配到棧數據結構里,因為一個棧的特點是第一個入棧的元素將會最后一個出棧。
這就好像酒店決定只能按照順序進行房間的出租。你沒辦法離開,除非你之前所有房間號比你大的人都走了。
所以,讓我們來想一下棧的操作過程。在很多操作系統中,每一個線程都有一個棧,并且棧的大小是一個可變的確定大小。當你調用一個函數的時候,相 關的內容被壓入棧內。當你把一個這個棧的指針傳出這個函數時,就像上面的提問者所干的一樣。那個指針只是指向全部有效的數百萬個字節內存塊的中間。在我們 的類比中,當你離開酒店的時候,你只是離開了當前被占用的數字最大的房間。如果沒有人在你之后入住,你又非法地回到了這個房間。你所有的東西肯定都還在這 個酒店的房間里。
我們用棧作為臨時存儲因為它們非常廉價并且容易實現。C++的實現沒有規定一定要用棧來存儲局部變量,你可以使用堆來存儲它們,不過沒有人這么干,因為那樣做會使得程序變得很慢。
C++也沒有規定在你離開棧之后需要清掉棧里的內容,所以你可以在之后非法地回到棧里找到你之前的內容。當然編譯器如果生成代碼,一旦你不再使用了就把棧里的所有內容都清零,這是完全合法的。不需要再解釋為什么了,因為這樣做代價非常高。
C++沒有規定要確保當棧變小時,之前有效的內存地址依然有效。C++的實現也允許告訴操作系統“我們已經不再需要棧的個內存頁了。除非我說, 否則當有任何人要訪問這個之前有效的棧的內存頁的時候拋出一個異常并結束程序”。再次,一般的實現也沒有這么做,因為這么說使程序變慢而且沒有必要。
相反,大多數時候,一般的 C++ 實現允許你犯錯然后避免它。直到有一天,一些真正非常令人恐怖的錯誤出現了然后把整個程序弄崩潰了。
這樣做是有問題的。C++里有如此多的規則而又如此輕易就可以打破它們。我自己就有好多次這樣的經歷。更糟的是,這種問題往往是表面的,當你發現內存地址沖突了之后去檢查內存,卻發現它們在很長時間內又是正確的。所以你很難知道到底是哪個地方出錯了。
那些內存安全的語言通過限制你的能力來解決這個問題。在規范的 C# 里,沒有任何辦法去獲取一個局部變量的內存地址,然后返回它或者是存儲它等以后再用。你可以獲取一個局部變量的內存地址,但是語言被很好的設計了,你不可 能在局部變量生命周期之后還能夠使用它。為了取得局部變量的內存地址并把它返回,你必須要把編譯器設置為一個特殊的不安全的模式,并且在你的程序里寫上 “unsafe”關鍵字。這可以幫助提醒你,你正在做一些不安全的可能會打破規則的事情。
更進一步閱讀:
當 C# 返回引用時做了些什么?
http://blogs.msdn.com/b/ericlippert/archive/2011/06/23/ref-returns-and-ref-locals.aspx
為什么我們用棧來管理內存?C#里值的類型是否一直存儲在棧里?虛擬內存是如何工作的?以及更多的關于 C# 內存管理是如何工作的。這里許多文章都對 C++ 程序員有幫助。
http://blogs.msdn.com/b/ericlippert/archive/tags/memory+management/
-
翻譯: 伯樂在線 - 菜鳥浮出水
譯文鏈接: http://blog.jobbole.com/69923/