C 語言局部變數,堆與棧的問題?
今天學C語言時遇到一個連我那科班出身,在中科院大學讀計算機專業碩士的高中同學都回答不了的問題,本人用的是visual studio2013專業版的C++模板來敲的C語言的代碼,
在上圖中,我在main函數代碼塊的外部聲明並定義了一個func函數,在func函數代碼塊里聲明了一個整型數組局部變數 i 並初始化為7,9,5 三個值,然後返回這個數組變數,接著在main函數代碼塊里聲明一個整型數組變數num並接收func函數的返回值,然後使用for循環依次列印num數組中的值,列印結果除了第一個值與數組 i 中的對應下標的值對應以外,其它均不相同,但我同學給的另外一種寫法卻可以實現,如下圖我同學的解釋是,數組的值都是在堆上開闢空間存儲的,在main函數中對func函數調用時,func函數的退出並不會清空數組 i 所對應的值,按理說,7,9,5這三個值是應該正常列印出來的。所以想問下各位技術大牛,背後的原因究竟是什麼?為什麼只能正確列印第一個值?為什麼第一個方法不能正常列印,第二個方法卻可以?在此感謝能抽出時間解疑的大牛。
這就類似於你在如家開了個房,突發奇想放了一百塊錢在枕頭底下,然後退房。過五分鐘你再從窗戶溜回你原來的房間:
- 可能發現那一百塊錢居然還在;
- 也可能發現那一百塊錢不見了;
- 甚至可能發現房間里已經有其他客人了,沒等你檢查那一百塊錢在不在你就被警察帶走了……
你現在的情況就屬於第一種一百塊錢還在的情況。用不著奇怪,如家從來沒有保證房間永遠保持你退房時的狀態。
所以這並不是你所說的「正確列印第一個值」,而是「列印出來的第一個值碰巧是 7」。
--
如果對象結束了生命期:訪問該對象會產生未定義行為;指向該對象的指針的值變為 indeterminate 值:
C99 §6.2.4/2(C11 措辭稍有不同)
An object exists, has a constant address, and retains its last-stored value throughout its lifetime. If an object is referred to outside of its lifetime, the behavior is undefined. The value of a pointer becomes indeterminate when the object it points to reaches the end of its lifetime.
所謂 indeterminate 的值,是一個未指定的值,或者「trap representation」:
C99 §3.17.2(C11 為 §3.19.2)
indeterminate value: either an unspecified value or a trap representation
所謂的 trap representation,是即便只是讀取一下都會產生未定義行為的神奇存在:
C99 §6.2.6.1/5(C11 同)
Certain object representations need not represent a value of the object type. If the stored value of an object has such a representation and is read by an lvalue expression that does not have character type, the behavior is undefined. If such a representation is produced by a side effect that modifies all or any part of the object by an lvalue expression that does not have character type, the behavior is undefined. Such a representation is called a trap representation.
--
p.s. 所謂的第二種寫法並不是 C 語言代碼,而是 C++ 代碼。
p.p.s 第一種寫法的那個數組對象的生命期在函數返回時結束;而 new / malloc 出來的對象,需要 delete / free 後才結束生命期。
p.p.p.s 你的同學可能 Java 寫多了……
new int……你這叫C語言?
中科院讀計算機分不清堆和棧?他是學Java的?
回答問題:堆的東西,不delete保證存在。棧的東西,函數返回了不保證存在。
為什麼能列印出第一個?編譯器說:我今天心情***,只留一個給你,不服你來打我啊
樓主的碩士同學可能畢業了想當產品經理
第一種是在堆棧空間開的,用static才是靜態數據(這裡之前說成堆有誤),用new申請才是堆。為什麼7能正常列印,那是堆棧是向下增長的。7放在堆棧低位置,9中間,5最高func堆棧布局應該如下:main函數返回值sp值597func函數返回之後,調用printf,壓入2個參數在printf的堆棧布局
*(num+i)
字元串地址main函數返回值sp值printf局部變數buf[1024]等。sp值不一定有,看有沒有優化。printf的臨時變數雖然覆蓋了7的位置,但是第一次printf,在函數調用之前,就把參數7壓入堆棧了。而printf調用列印出7之後,就把全部破壞了。如果代碼修改下,i改成從2到0,就會列印5,而9,7列印不出來。或者改成printf("%d,%d,%d",*num,*(num+1),*(num+2)),就都能顯示出來。他是學java的吧
清空不清空由編譯器自己決定,不要這麼搞
我是一個國王,我的名字叫i,一個蠢蠢的程序員製造了我,讓我管理棧中的一塊內存。還給我分配了7,9,5三個平民。我讓他們分別住在(i+0),(i+1),(i+2)三個地方。
有一天,這個程序員剝奪了我管理這篇土地的權利,所有事情都脫離了我的掌控。我已經沒有能力保護我的子民7,9,5了。與此同時,外部勢力大舉入侵他們在這片土地上肆意建造、銷毀。我的子民生死未卜。
後來,這個程序員來找我了,他問我他讓我管理的7,9,5呢。其實我也不知道,因為那個地方已經不歸我管了。我只能告訴他,他們曾經住在(i+0),(i+1),(i+2)。這個程序員就去這三個地方去訪問他們,結果他發現只有7還在那,9跟5不翼而飛了。
我是一個數字7,一個蠢蠢的程序員製造了我,並且讓我跟著國王i。這個國王很好,讓我住在了(i+0)這個地方,我的旁邊住著熱情了9和5。平時我們無聊會在一起鬥鬥地主,聊聊數生理想。
突然有一天,程序員剝奪了國王的管理我們的權利,我,鄰居9和5以及住在這個王國的其他數字還沒來得及轉移。大批外部勢力入侵,燒殺搶掠。我的鄰居9和5被他們殘忍殺害,847978323和-2奪走了他們的住所。可能是我存在感太弱,他們沒有注意到我,所以我才能一直苟活至今。但是我也不知道哪一天會跟9和5一樣,被不知名的數字取代。
後來,那個程序員來找過我,他還問我9和5呢。他說,為什麼你還在,9和5卻不見了。他的說話口氣讓我很不舒服,似乎我的存在就是一個錯誤。這讓我想起了《被嫌棄的松子的一生》,生而為數,十分抱歉。可是我能怎麼回答呢?那一刻,我的國王i失去管理權的那一刻,我就已經失去了存在的意義,我們已經永遠失去了價值。程序員見我不說話,就去隔壁問那個新來的數字了。
我是一個數字847978323,有一天一個程序員來問我。數字9呢?我當時就覺得這個人很奇怪,一定是腦子壞掉了,然後我沒有理他。隨後就到m_iNode將軍那裡去報到了。
我是一個國王,我的名字叫i,這一次程序員讓我管理堆中的一塊內存,堆跟棧不同,在堆這個世界中很注重個人財產權,只要你還住在這個地方就沒有人可以趕走你。正是這個原因,我想這次我總不會因為程序員自己的失誤而受到責備了吧。這一次依然住來了7,9,5三個數字。程序員似乎很滿意,因為他發現無論什麼時候,他的7,9,5都在。後來他慢慢的開始遺忘了我,一次偶然事件中,他不小心寫下了i = 0;呵呵,去他媽的程序員,你再也找不到你的7,9,5了。
我是一個數字7,我再一次被製造了出來,很幸運的是,這一次我又跟9和5做了鄰居。閑暇時間我們還是會經常聊人生,鬥地主。不過,前不久我們的國王失蹤了。據過往的數字們說,他們見過我們的國王,他似乎已經變成了NULL,他們都對此表示了同情。然而最慘的並不是我們的國王,因為我們覺得即便如此,我們的國王在程序結束的時候依然可以正常被釋放。而我們仨就不同了,除非關機,否則我們他媽要在這個鬼地方斗一輩子的地主!!!
我更傾向於說這是【作用域】的問題。第一種寫法 func 裡面的數組 i 會在函數結束後被銷毀,第二種寫法 func 裡面的 i 不是數組。你可以嘗試在第一種寫法里把數組 i 的定義移到 func 前面。
第一種情況,你在func()函數中定義了一個靜態數組,數組在棧內存中開闢內存空間來存儲數組元素,當func()函數運行結束時棧內存銷毀,而你返回的數組名只是一個指向該內存的地址,所以你返回的是一個指向內存已經銷毀的地址,只是碰巧第一個元素碰對了,該內存中的值是不確定的。
第二種情況,使用new運算符開闢了動態內存空間,數組元素存儲在堆內存中,不使用delete運算符回收空間之前,該空間內存中的元素一直在,c++堆內存不會自動回收,所以在主函數中接收的指針指向的內存空間依然有效。所以發生了你截圖中的情況,根據你同學的解釋,可能java寫多了,不了解c++的內存機制吧。要解釋這個問題, 我們需要了解以下事實,
- 一般的運行在x86 架構上的high-level的程序語言, 比如C或者C++等,裡面的方程(不管是function還是method等,即你的代碼里的func()和main()) 都是通過棧(stack)來實現的.
- 每條線程(thread)都有且只有一個棧(stack) -- 這是顯然的,因為CPU的寄存器裡面只有一組棧指針(即, esp和ebp -- 且用x86-32位來說)
- 基本方式是, 每個方程(即function being called)開始的第一步,都會把現有的棧(即function making the call)的指針ebp存起來(存在當前的棧上),這個代碼一般是由編譯器自動生成的(只要你寫了一個function, 編譯器都會生成這個步驟的代碼,這個步驟一般被稱為function prologue).
- function prologue通常看起來是這個樣子(這兒用的intel 句法):
push ebp ;; 保存前一個方程(即function making the call)棧基指針
mov ebp, esp ;; 將當前棧頂指針作為新的方程(即function being called)的棧基指針
sub esp, N ;; 分配新的棧的空間(通過增加棧頂指針值 -- 棧一般在內存的位置是從高地址往低地址方向擴張,所以這裡用的減法)
;; 新的棧的空間作為方程裡面定義的變數的(一般的, 非靜態的; 靜態的[即static]變數不會放在棧上)
5. 當方程調用結束時, 通過調整棧指針來釋放當前的方程的棧的空間. (即上面3和4過程的逆過程). 這樣當被調用的方程(即function being called)返回後,棧收縮到調用方程(function making the call)的位置上. 這個步驟的代碼一般也是有編譯器生成的(只要你寫了一個function, 編譯器都會生成這個步驟的代碼,這個步驟一般被稱為function epilogue).這個過程通常看起來是這個樣子的:
mov esp, ebp ;; 將棧基指針值移動到棧頂指針去, 這樣calling function的棧頂就找到了.
pop ebp ;; 將前面存起來的方程的棧基指針取出來 (即對應function prologue的 "push ebp")
;; 所以我們看到 這兩個步驟完全是function prologue的前面兩個步驟的對稱的逆過程.
ret ;; 將方程返回地址值彈到代碼指針裡面去(對應彙編里的call 指令, 詳細的請 RTFM ).
有了以上的幾個事實,你提供的程序的問題就比較容易理解了...
在第一段代碼中,你的_tmain()方程調用了func()方程, 然後把func()方程的返回值(即 整數 7)保存到 num 變數裡面去. num變數在_tmain()方程的stack裡面一直保留著. 所以你得到了這個結果。
在你的第二段代碼中, 你的_tmain()方程調用了func()方程...然後把func()方程的返回值保存到num變數裡面去. 注意,這個時候你func()方程的返回值是一個它被調用過程中的它自身的棧上的地址(即指向數組{7, 9, 5}). 在func()方程返回到_tmain()方程裡面以後,這個地址值被保存到了num變數中. 但是這個時候的棧指針已經恢復到了_tmain()方程的棧,而不是func()方程的棧. 這裡另外一個需要注意的地方是, 方程返回後,其棧上的變數不會被它自己或者它的調用的方程來overwrite...它的棧上的變數的overwrite的是由其調用方程來調用一個新的方程,由這個新的方程生成新的它自己的棧並且給它的棧上的變數賦值來完成的。這樣,當_tmain()方程調用printf()方程的時候, 原來的func()方程的內部變數值(即在這些變數原來的地址上的那些值)就會被overwrite. 所以,你看到的那個847978323實際上是第一次調用printf後的原地址上的殘留值,而-2(即0xfffffffe)是第二次調用printf()後的原地址上的殘留值。
這個時候,有些喜歡惡意提問的同學就會問了, 那為什麼我們還能在第一次循環的時候仍能得到7呢? 為什麼這個7值沒有被 printf()的調用overwrite呢? 這實際上要從方程調用時候的參數傳遞來理解. 方程的參數的傳遞是發生在在其開始運行之前的(由編譯器生成代碼來拷貝參數). 也就是說, 這個7的值在printf()在第一個循環中開始執行之前就已經被拷貝出來了.
我把你的程序稍微修改了一下...這樣你可以看到. 在第一個循環的時候,i = 0, 兩個num_func變數所存地址上的值是不同的(大概率的不同)...這是因為在列印這兩個num_func的值的過程中間,發生了一次printf()調用, 把原來的7所對應的地址上所存的內容(即整數7)給overwrite了....
#include &
int* func(){
int i[] = {7, 9, 5};
return i;
}
int main(int argc, char **argv){
int i;
int* num_func;
int num_array[3];
num_func = func();
num_array[0] = *num_func;
num_array[1] = *(num_func + 1);
num_array[2] = *(num_func + 2);
for(i = 0; i &< 3; i++){
printf("num_func[%d] -&> %d
", i, *(num_func + i));
printf("num_func[0] -&> %d
", i, *num_func);
printf("=====
");
}
printf("===================================================
");
for(i = 0; i &< 3; i++){
printf("num_array[%d] -&> %d
", i, *(num_array + i));
}
return 0;
}所存的內容7
第一種方法開闢的數組你會手動delete嗎?不會對吧,那它就是棧上開闢的。然後函數返回後局部變數的行為就變成未知的了。
第二種是new的,就肯定要delete。那麼就是在堆上開闢,這樣就不會被銷毀也能被列印出來。對象的作用域
int [ ] 是數組類型,在傳遞的時候會轉換成指向數組首元素的指針
你在局部變數裡面定義了一個數組,離開作用域時,這個數組會被銷毀。
返回右值和左值
你選擇返回了一個int *,這是一個右值傳遞,也就是說,return那一瞬間,被return的那個對象發生了一次拷貝賦值,拷貝出的副本(一個指向數組首元素的指針)被返回了,而原件(整個數組)被銷毀了(因為離開了作用域)
看看你到底傳遞(return)了什麼
你定義了一個int [ ]類型對象,這個對象在傳遞時被轉換成了int*。離開了作用域之後int [ ]類型的數組被銷毀,只剩一個指針(地址值)被傳了出去。明白了嗎?你返回了一個指向已經銷毀對象的指針。
new做了什麼
想一想,new int(10)是什麼類型?int *類型。new char(A)呢?char*類型。那new int [ ]{7,8,9}呢?int(*)[ ]類型咯。int [ ]會在傳遞時轉換成什麼?int*。那回頭想想 int(*)[ ]是什麼類型?int**。
new返回的時候做了什麼
i 的地址值是指向一個數組對象的。i 在離開作用域的時候會被銷毀,注意,i 銷毀僅僅是一個指針被銷毀,指針指向的內容還在堆內存里,這也就是為什麼保證要delete每一個new出來的內存,只有delete才能銷毀new指針指向的對象,不delete就會出現內存里有數據但是沒有指針指向它的情況(內存泄漏)。i 雖然被銷毀了,i 的對象還在,i 的值(也就是數組對象的地址值)卻被傳了出來,這時候你再按照那個地址值找,當然能找到尚未被銷毀的數組。
————————————————
new一個數組會出來什麼類型,評論有另外的見解,歡迎大家來討論
居然可以運行完,在我這邊(gcc version 6.2.0 (x86_64-posix-seh-rev1, Built by MinGW-W64 project)),是會在編譯時拋出一個警告:
warning: function returns address of local variable [-Wreturn-local-addr]
return i;
^
然後在運行時爆掉的。
警告說的很清楚了,func 函數返回的是一個指向本地數組的指針(C的一種語法糖,返回數組名或利用數組名進行指針運算時,會幫你隱式轉化成指向該數組的指針)。
然後在 main 函數中又作死般地訪問了一個已經不在棧上的地址,因為 func 函數返回後其對應的棧幀已經彈出 stack 了。這就像懸垂指針,你 free 了以後再訪問。。。不過你這種操作在某些情況下更危險,操作系統不會允許的,一般直接爆。
還有 C++ 和 C 是兩門語言,沒有那種直接在 C++ 中寫 C 的操作,要寫就 extern "C" {} 。。。
new的會在堆上,你不去釋放他會一直在的,就像你寫一個whiletrue,一直new不去釋放的話最後會崩掉的。另一種情況再你函數執行完畢了,我地址內的空間會被釋放,但是你要是在訪問地址的話肯定會有東西,但是我不保證是什麼東西我不確定7是偶發現象還是必然現象,想試一試~~~
你用new申請的內存空間在delete之前應該都是存在的,所以實際大項目有內存溢出的問題。而其他的兩個是單純局部變數,操作系統為你分配空間,在堆裡面,在退出fun函數之後,操作系統會自動釋放,你能列印出來第一個也是偶然,有時成功有時不成功,打代碼千萬別這麼寫,這種bug有時出現有時不出現。如有錯誤,請指正。
最頂樓的答案,說的很明白了。想仔細看堆棧的信息,可以把斷點設置在函數調用的地方,單步調試看看。
在棧上分配的內存在函數返回後無效,在堆上分配的內存沒有delete之前可以訪問
。。。。。自動變數是在棧上啊,函數內的變數要給其他函數調用要加static 或頭文件下面全局變數,哪有這麼挖坑往裡面挑的
你需要malloc……我們大二的教養課剛講完啊………new的話就是c++了吧……建議不要用各種能識別c++的學c……最好是自己gcc
你的代碼的問題是變數作用域的問題,你定義的那個數組在函數調用結束後,內存會被回收,你同學的正確是因為new或malloc分配的內存在整個程序生命周期都有效,只有free或程序結束後,才會釋放內存
推薦閱讀:
※C++ 內置變數字元串有什麼好的實現思路?
※怎麼讓我寫出個零位元組的類了?
※各位知友都喜歡用什麼IDE?
※把當前各種編程語言的優秀特性集中到一起,設計一種最好的語言,是否可行?
※如何用C++從API開始,寫一款Windows上的視頻播放器?