酒店房間和 C++ 區域性變數的作用域

jobbole發表於2014-06-06

  問題: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/

  原文連結: StackOverflow   翻譯: 伯樂線上 - 菜鳥浮出水

相關文章