2010-05-03 70 views
148

如果我在一組新的花括號中創建一個變量,那個變量是從右花括號中的堆棧彈出的,還是掛出直到函數結束?例如:在C中,大括號是作爲一個堆棧框架嗎?

void foo() { 
    int c[100]; 
    { 
     int d[200]; 
    } 
    //code that takes a while 
    return; 
} 

dcode that takes a while節期間佔用內存?

+7

您是指(1)根據標準,(2)通用實踐中的實踐,或者(3)實現中的常見實踐? – 2010-05-03 20:40:47

回答

77

不,大括號不作爲堆棧框架。在C中,大括號只是表示一個命名範圍,但是沒有任何東西會被銷燬,也不會在控制通過時彈出堆棧。

作爲一名編程人員編寫代碼,您經常可以將其看作是一個堆棧框架。在大括號中聲明的標識符只能在大括號中訪問,所以從程序員的角度來看,它們就像它們在聲明時被壓入堆棧,然後在範圍退出時彈出。但是,編譯器不必生成在入口/出口處推出/彈出任何東西的代碼(並且通常不會)。

另請注意,局部變量根本不會使用任何堆棧空間:它們可以保存在CPU寄存器或其他輔助存儲位置,也可以完全優化。

因此,理論上d數組可能會消耗整個函數的內存。但是,編譯器可能會優化它,或與其使用壽命不重疊的其他本地變量共享其內存。

+9

這不是特定於實現的嗎? – avakar 2010-05-03 16:06:00

+1

它在C++ /其他語言中的工作方式不同嗎? – 2010-05-03 16:06:22

+52

在C++中,對象的析構函數在其作用域的末尾被調用。內存是否被回收是一個特定於實現的問題。在C++中使用 – 2010-05-03 16:07:21

0

我相信它會超出範圍,但不會彈出堆棧直到函數返回。所以它仍然會佔用堆棧中的內存,直到函數完成,但不能在第一個閉合大括號的下游訪問。

+3

無法保證。一旦範圍關閉,編譯器不再跟蹤該內存(或者至少不需要......),並且可以很好地重用它。這就是爲什麼觸摸以前被超出範圍變量佔用的內存是未定義的行爲。小心鼻子惡魔和類似的警告。 – dmckee 2010-05-03 17:02:47

6

它取決於實現。我寫了一個簡短的程序來測試gcc 4.3.4的功能,並且在函數的開始時一次性分配所有的堆棧空間。您可以使用-S標誌檢查gcc生成的程序集。

3

不,d []將而不是在例程的其餘部分處於堆棧上。但是alloca()是不同的。

編輯: Kristopher約翰遜(和Simon和Daniel)是,和我最初的反應是。與海灣合作委員會4.3.4.on CYGWIN,代碼:

void foo(int[]); 
void bar(void); 
void foobar(int); 

void foobar(int flag) { 
    if (flag) { 
     int big[100000000]; 
     foo(big); 
    } 
    bar(); 
} 

給出:

_foobar: 
    pushl %ebp 
    movl %esp, %ebp 
    movl $400000008, %eax 
    call __alloca 
    cmpl $0, 8(%ebp) 
    je  L2 
    leal -400000000(%ebp), %eax 
    movl %eax, (%esp) 
    call _foo 
L2: 
    call _bar 
    leave 
    ret 

生活和學習!一個快速測試似乎表明,AndreyT對於多個分配也是正確的。

稍後添加:以上測試顯示gcc documentation不太正確。多年來它已經表示(強調):

「的一個可變長度數組的空間是只要陣列名稱的範圍結束釋放」。

+0

禁用優化編譯並不一定會向您顯示優化代碼中的內容。在這種情況下,行爲是相同的(在函數的開始時分配,並且在離開函數時只有空閒):https://godbolt.org/g/M112AQ。但非cygwin gcc不會調用'alloca'函數。我真的很驚訝,cygwin gcc會這樣做。它甚至不是一個可變長度的數組,所以IDK爲什麼要提出這個數組。 – 2017-07-30 02:27:47

18

您的問題不夠明確,無法明確回答。

一方面,編譯器通常不會爲嵌套塊作用域執行任何本地內存分配 - 解除分配。本地內存通常只在函數入口處分配一次,並在函數出口釋放。另一方面,當本地對象的生命週期結束時,該對象佔用的內存可以在稍後重新用於其他本地對象。例如,在此代碼

void foo() 
{ 
    { 
    int d[100]; 
    } 
    { 
    double e[20]; 
    } 
} 

兩個陣列通常佔用相同的存儲器區域,這意味着本地存儲的由函數foo所需要的總量爲任何必要爲兩個最大陣列,而不是爲他們在同一時間。

是否後者符合d繼續佔用內存,直到問題的上下文結束爲止由您來決定。

1

您的變量d通常不會彈出堆棧。大括號不表示堆棧框架。否則,你就無法做這樣的事情:

char var = getch(); 
    { 
     char next_var = var + 1; 
     use_variable(next_char); 
    } 

如果花括號引起了真正的壓棧/流行(如函數調用會),那麼上面的代碼將無法編譯,因爲裏面的代碼大括號將無法訪問大括號外的變量var(就像子函數不能直接訪問調用函數中的變量)。我們知道情況並非如此。

花括號僅用於範圍界定。編譯器會將對內部變量的任何訪問視爲無效,並可能將該內存重用於其他內容(這取決於實現)。但是,在封閉函數返回之前,它可能不會從堆棧彈出。

更新:以下是C spec必須說的。關於具有自動存儲持續時間的對象(見第6.4.2節):

對於一個對象,該對象不具有可變長度數組類型,其 壽命從進入與它相關聯 直到執行所述塊延伸無論如何,該塊終止。

期間 ,其存儲爲 保證

對象的壽命是程序執行的部分:

相同的部分定義的術語「壽命」爲(重點煤礦)爲它保留。存在一個對象, 具有一個固定的地址,並在整個 的生存期中保留其最後存儲的值。如果一個對象在其生命週期之外被引用,那麼 行爲是未定義的。

這裏的關鍵詞當然是「有保證的」。一旦你離開了內部括號的範圍,數組的壽命就結束了。存儲可能還是不會被分配給它(您的編譯器可能會重新使用該空間作爲其他東西),但是任何訪問數組的嘗試都會調用未定義的行爲並導致不可預知的結果。

C規範沒有堆棧幀的概念。它只談到結果程序的行爲方式,並將實現細節留給編譯器(畢竟,在無堆棧CPU上的實現與在具有硬件堆棧的CPU上的實現看起來完全不同)。 C規範沒有規定堆棧幀將會或不會結束的地方。唯一需要知道的真實的方法是編譯您的特定編譯器/平臺上的代碼並檢查生成的程序集。編譯器當前的一組優化選項也可能在這方面發揮作用。

如果你想確保陣列d不再吃了,而你的代碼運行內存,您可以在大括號中的代碼轉換成一個單獨的函數或明確mallocfree內存,而不是使用自動存儲。

+1

_「如果大括號導致了堆棧的push/pop,那麼上面的代碼就不會編譯,因爲大括號內的代碼將無法訪問大括號外的變量var」_--這是不正確的。編譯器可以始終記住堆棧/幀指針的距離,並使用它來引用外部變量。 此外,請參閱約瑟夫的答案爲大括號的一個例子,_do_導致堆棧推/流行。 – george 2012-02-08 20:17:41

+0

@ george-您描述的行爲以及Joseph的示例取決於您正在使用的編譯器和平臺。例如,爲MIPS目標編譯相同的代碼會產生完全不同的結果。我純粹從C規範的角度講話(因爲OP沒有指定編譯器或目標)。我將編輯答案並添加更多細節。 – bta 2012-02-14 18:48:39

1

他們可能。他們可能不會。我認爲你真的需要的答案是:永遠不要假設任何事情。現代編譯器可以完成各種架構和特定實現的魔法。把你的代碼簡單易懂地寫給人類,讓編譯器做好這些事情。如果你試圖在編譯器周圍編寫代碼,那麼你會遇到麻煩 - 而且在這些情況下通常會遇到的麻煩通常非常微妙且難以診斷。

36

變量爲實際上佔用內存的時間顯然依賴於編譯器(並且許多編譯器在內部塊被輸入並退出函數時不會調整堆棧指針)。

然而,密切相關,但可能更有趣的問題是該程序是否被允許訪問該內部範圍(但包含函數內),即外該內對象:

void foo() { 
    int c[100]; 
    int *p; 

    { 
     int d[200]; 
     p = d; 
    } 

    /* Can I access p[0] here? */ 

    return; 
} 

(換言之:編譯器允許釋放d,即使在實踐中大多數不會?)。

答案是編譯器允許解除分配d,和訪問p[0]其中註釋表明是未定義的行爲(程序是允許訪問內範圍之外的內對象)。 C標準的相關部分是6.2。4P5:

對於這樣的對象[一個具有 自動存儲持續時間],做 不具有可變長度數組類型, 其壽命從進入與它相關聯 直到執行所述塊延伸該塊的結尾爲 任何方式。 (輸入一個封閉的塊 或調用一個函數暫停,但 不會結束,執行當前的 塊。)如果塊以遞歸方式輸入 ,則每次都會創建一個新的 對象實例。 對象的初始值是 不確定。如果爲該對象指定了一個初始化爲 ,則在執行該塊時每執行一次聲明爲 就執行 ; 否則,每次達到 聲明時,該值將變爲 不確定。

+0

作爲學習如何範圍和內存在C和C++多年使用高級語言後工作的人,我發現這個答案比接受的更精確和有用。 – Chris 2018-02-10 23:31:50