簡體   English   中英

C ++中的堆棧,靜態和堆

[英]Stack, Static, and Heap in C++

我已經搜索過了,但我對這三個概念並不是很了解。 我何時必須使用動態分配(在堆中)以及它的真正優勢是什么? 靜態和堆棧有什么問題? 我可以編寫整個應用程序而無需在堆中分配變量嗎?

我聽說其他語言包含了“垃圾收集器”,所以你不必擔心內存。 垃圾收集器做什么?

您可以自己操作內存,而不能使用此垃圾收集器嗎?

有人告訴我這個聲明:

int * asafe=new int;

我有一個“指針指針”。 這是什么意思? 它不同於:

asafe=new int;

有一個類似的問題被問到,但它沒有詢問靜力學。

靜態,堆和堆棧內存的摘要:

  • 靜態變量基本上是一個全局變量,即使您無法全局訪問它。 通常,可執行文件本身就有一個地址。 整個程序只有一個副本。 無論你進入函數調用(或類)多少次(以及多少個線程!),變量都指的是相同的內存位置。

  • 堆是一堆可以動態使用的內存。 如果你想要4kb的對象,那么動態分配器將查看堆中的可用空間列表,選擇一個4kb的塊,並將其提供給你。 通常,動態內存分配器(malloc,new等)從內存結束開始並向后工作。

  • 解釋堆棧如何增長和縮小有點超出了這個答案的范圍,但足以說你總是只添加和刪除。 堆棧通常從高處開始並向下擴展到較低的地址。 當堆棧遇到中間某處的動態分配器時,內存不足(但請參考物理內存與虛擬內存和碎片)。 多個線程將需要多個堆棧(該過程通常為堆棧保留最小大小)。

當你想要使用每一個時:

  • 靜態/全局變量對於你知道你將永遠需要的內存非常有用,並且你知道你不想要釋放。 (順便說一句,嵌入式環境可能被認為只有靜態內存......堆棧和堆是第三種內存類型共享的已知地址空間的一部分:程序代碼。程序通常會動態分配它們的靜態內存,當他們需要像鏈接列表這樣的東西時。但無論如何,靜態內存本身(緩沖區)本身並不是“已分配”,而是為了這個目的而將其他對象分配給緩沖區所占用的內存。你可以這樣做在非嵌入式游戲中,控制台游戲將經常避開內置的動態內存機制,有利於通過使用預設大小的緩沖區來嚴格控制分配過程。)

  • 當你知道只要函數在范圍內(在某個堆棧上)時,堆棧變量就很有用,你會希望保留變量。 堆棧適用於它們所在的代碼所需的變量,但在該代碼之外不需要。 當您訪問資源(如文件)時,它們也非常適合您,並希望在您離開該代碼時資源自動消失。

  • 當您希望比上述更靈活時,堆分配(動態分配的內存)非常有用。 通常,調用函數來響應事件(用戶單擊“創建框”按鈕)。 正確的響應可能需要分配一個新對象(一個新的Box對象),該對象應該在函數退出后很長時間內保持不變,因此它不能在堆棧中。 但是你不知道在程序開始時你想要多少個盒子,所以它不能是靜態的。

垃圾收集

我最近聽說過垃圾收集器有多棒,所以也許有點不同意見會有所幫助。

當性能不是一個大問題時,垃圾收集是一種很好的機制。 我聽說GC正在變得更好,更復雜,但事實是,你可能會被迫接受性能損失(取決於用例)。 如果你很懶,它仍然可能無法正常工作。 在最好的時候,垃圾收集器意識到當你意識到沒有更多的引用時你的記憶會消失(參見引用計數 )。 但是,如果你有一個引用自身的對象(可能通過引用另一個引用的對象),那么單獨引用計數並不表示可以刪除內存。 在這種情況下,GC需要查看整個參考湯,並確定是否有任何島嶼僅由他們自己引用。 另外,我猜這是一個O(n ^ 2)操作,但不管它是什么,如果你完全關心性能,它會變壞。 (編輯:Martin B 指出 ,對於合理有效的算法,它是O(n)。如果你關注性能,那么它仍然是O(n)太多,並且可以在沒有垃圾收集的情況下在恆定時間內解除分配。)

就個人而言,當我聽到人們說C ++沒有垃圾收集時,我的腦海中將其標記為C ++的一個特征,但我可能只是少數。 人們學習C和C ++編程最困難的可能是指針以及如何正確處理動態內存分配。 如果沒有GC,其他一些語言(如Python)會很糟糕,所以我認為這取決於你想要的語言。 如果你想要可靠的性能,那么沒有垃圾收集的C ++是我能想到的Fortran這一方面唯一的東西。 如果您想要易於使用和訓練輪(為了避免您在不需要學習“正確”內存管理的情況下崩潰),請選擇GC。 即使您知道如何妥善管理內存,也可以節省您優化其他代碼的時間。 實際上沒有太多的性能損失了,但是如果你真的需要可靠的性能(並且能夠准確地知道發生了什么,何時,在幕后)那么我會堅持使用C ++。 有一個原因,我聽說過的每個主要游戲引擎都是C ++(如果不是C或匯編)。 Python等人可以很好地編寫腳本,但不是主要的游戲引擎。

以下當然都不太准確。 當你讀它時帶上一粒鹽:)

嗯,你提到的三件事是自動,靜態和動態存儲持續時間 ,這與物體生存時間和生命開始時間有關。


自動存儲持續時間

您對短期小型數據使用自動存儲持續時間,僅在某些塊中本地需要:

if(some condition) {
    int a[3]; // array a has automatic storage duration
    fill_it(a);
    print_it(a);
}

一旦我們退出塊,生命周期就會結束,並且一旦定義了對象就會立即開始。 它們是最簡單的存儲持續時間,並且比特定的動態存儲持續時間更快。


靜態存儲持續時間

您對自由變量使用靜態存儲持續時間,任何代碼都可以訪問它們,如果它們的范圍允許這樣的使用(命名空間范圍),以及需要在其范圍的退出(本地范圍)上延長其生命周期的局部變量,以及對於需要由其類的所有對象(類范圍)共享的成員變量。 它們的生命周期取決於它們所處的范圍。它們可以具有命名空間范圍本地范圍以及類范圍 這兩者的真實之處在於,一旦他們的生命開始,生命在計划結束時結束 這是兩個例子:

// static storage duration. in global namespace scope
string globalA; 
int main() {
    foo();
    foo();
}

void foo() {
    // static storage duration. in local scope
    static string localA;
    localA += "ab"
    cout << localA;
}

該程序打印ababab ,因為localA在其塊退出時不會被銷毀。 您可以說當控件達到其定義時 ,具有局部范圍的對象開始生命周期。 對於localA ,它會在輸入函數體時發生。 對於命名空間范圍內的對象,生命周期從程序啟動開始 對於類范圍的靜態對象也是如此:

class A {
    static string classScopeA;
};

string A::classScopeA;

A a, b; &a.classScopeA == &b.classScopeA == &A::classScopeA;

如您所見, classScopeA並未綁定到其類的特定對象,而是綁定到類本身。 上面所有三個名稱的地址是相同的,都表示相同的對象。 有關靜態對象何時以及如何初始化的特殊規則,但現在不要關注它。 這意味着術語靜態初始化順序慘敗


動態存儲持續時間

最后的存儲持續時間是動態的。 如果你想讓對象在另一個島上生存,你想使用它,並且你想要引用它們引用它們。 如果對象很大 ,並且想要創建僅在運行時已知的大小數組,也可以使用它們。 由於這種靈活性,具有動態存儲持續時間的對象復雜且管理緩慢。 具有該動態持續時間的對象在發生適當的操作符調用時開始生命周期:

int main() {
    // the object that s points to has dynamic storage 
    // duration
    string *s = new string;
    // pass a pointer pointing to the object around. 
    // the object itself isn't touched
    foo(s);
    delete s;
}

void foo(string *s) {
    cout << s->size();
}

它的生命周期只有在你為它們調用delete時才會結束。 如果你忘記了,那些對象永遠不會終結。 定義用戶聲明的構造函數的類對象不會調用其析構函數。 具有動態存儲持續時間的對象需要手動處理其壽命和相關的存儲器資源。 存在庫以便於使用它們。 可以使用智能指針建立特定對象的 顯式垃圾回收

int main() {
    shared_ptr<string> s(new string);
    foo(s);
}

void foo(shared_ptr<string> s) {
    cout << s->size();
}

您不必關心調用delete:如果引用該對象的最后一個指針超出范圍,則共享ptr會為您執行此操作。 共享ptr本身具有自動存儲持續時間。 因此它的生命周期是自動管理的,允許它檢查是否應該刪除其析構函數中指向動態對象的內容。 有關shared_ptr參考,請參閱增強文檔: http//www.boost.org/doc/libs/1_37_0/libs/smart_ptr/shared_ptr.htm

精心設計,就像“簡短回答”:

  • 靜態變量(類)
    lifetime =程序運行時(1)
    visibility =由訪問修飾符(private / protected / public)確定

  • 靜態變量(全局范圍)
    lifetime =程序運行時(1)
    visibility =在(2)中實例化的編譯單元

  • 堆變量
    lifetime =由您定義(新刪除)
    visibility =您定義的(無論您指定指針的是什么)

  • 堆棧變量
    visibility =從聲明到退出范圍
    lifetime =從聲明直到聲明范圍退出


(1)更確切地說:從初始化到編譯單元的去初始化(即C / C ++文件)。 標准未定義編譯單元的初始化順序。

(2)注意:如果在頭文件中實例化靜態變量,每個編譯單元都會獲得自己的副本。

我敢肯定其中一個學生會很快得到一個更好的答案,但主要區別在於速度和體型。

分配速度更快。 它在O(1)中完成,因為它是在設置堆棧幀時分配的,因此它基本上是免費的。 缺點是,如果你的堆棧空間不足,你就會被剔除。 你可以調整堆棧大小,但IIRC你可以玩大約2MB。 此外,一旦退出該功能,堆棧上的所有內容都將被清除。 因此,稍后引用它可能會有問題。 (指向堆疊已分配對象的指針會導致錯誤。)

分配速度極慢。 但你有GB玩,並指向。

垃圾收集器

垃圾收集器是一些在后台運行並釋放內存的代碼。 當你在堆上分配內存時,很容易忘記釋放它,這被稱為內存泄漏。 隨着時間的推移,應用程序消耗的內存會增長並增長,直到崩潰為止。 讓垃圾收集器定期釋放你不再需要的內存有助於消除這類錯誤。 當然,這需要付出代價,因為垃圾收集器減慢了速度。

靜態和堆棧有什么問題?

“靜態”分配的問題在於分配是在編譯時進行的:您不能使用它來分配一些可變數量的數據,其數量在運行時才知道。

分配“堆棧”的問題是,一旦執行分配的子例程返回,分配就會被銷毀。

我可以在堆中沒有分配變量的情況下編寫整個應用程序嗎?

也許但不是一個非平凡的,正常的,大的應用程序(但是所謂的“嵌入式”程序可能在沒有堆的情況下使用C ++的子集編寫)。

什么垃圾收集器呢?

它會持續監視您的數據(“標記和掃描”),以檢測您的應用程序何時不再引用它。 這對應用程序來說很方便,因為應用程序不需要釋放數據......但垃圾收集器的計算成本可能很高。

垃圾收集器不是C ++編程的常用功能。

您可以自己操作內存,而不能使用此垃圾收集器嗎?

學習確定性內存釋放的C ++機制:

  • '靜態':從未取消分配
  • 'stack':變量“超出范圍”
  • 'heap':當指針被刪除時(由應用程序顯式刪除,或在某些或其他子例程中隱式刪除)

當堆棧太“深”並且溢出可用於堆棧分配的內存時,堆棧內存分配(函數變量,局部變量)可能會出現問題。 堆用於需要從多個線程或整個程序生命周期中訪問的對象。 您可以在不使用堆的情況下編寫整個程序。

沒有垃圾收集器,您可以非常輕松地泄漏內存,但您也可以指定何時釋放對象和內存。 我在運行GC時遇到了Java問題並且我有一個實時進程,因為GC是一個獨占線程(沒有別的東西可以運行)。 因此,如果性能至關重要並且您可以保證沒有泄漏的對象,則不使用GC非常有幫助。 否則,當您的應用程序消耗內存並且您必須追蹤泄漏源時,它只會讓您討厭生命。

如果您的程序不知道要分配多少內存(因此您不能使用堆棧變量),該怎么辦? 說鏈接列表,列表可以增長,而不知道它的大小是什么。 因此,當您不知道將多少元素插入其中時,在堆上進行分配對於鏈表是有意義的。

在某些情況下GC的一個優點是其他人的煩惱; 對GC的依賴鼓勵人們不要多考慮它。 理論上,等到“空閑”期間或直到絕對必須,它會竊取帶寬並導致應用程序中的響應延遲。

但你不必“不去想它”。 就像多線程應用程序中的其他所有內容一樣,當您可以屈服時,您可以屈服。 例如,在.Net中,可以請求GC; 通過這樣做,而不是更頻繁地運行更長時間的GC,您可以更頻繁地縮短運行GC,並分散與此開銷相關的延遲。

但這打敗了GC的主要吸引力,似乎“鼓勵他們不必多思考它,因為它是自動化的。”

如果你在GC變得普遍並且對malloc / free和new / delete感到滿意之前第一次接觸到編程,那么甚至可能會發現GC有點煩人和/或不信任(因為人們可能不信任'優化,'歷史悠久。)許多應用程序容忍隨機延遲。 但對於沒有隨機延遲不太可接受的應用程序,常見的反應是避開GC環境並朝着完全不受管理的代碼的方向移動(或上帝禁止,一種長期垂死的藝術,匯編語言。)

我有一個夏天的學生在這里,一個實習生,聰明的孩子,誰在GC斷奶; 他非常贊賞GC的優勢,即使在非托管C / C ++編程時,他拒絕遵循malloc / free new / delete模型,因為引用“你不應該用現代編程語言來做這件事。” 而且你知道? 對於小型,短期運行的應用程序,您確實可以逃脫,但不適用於長期運行的高性能應用程序。

堆棧是由編譯器分配的內存,當我們編譯程序時,默認編譯器從OS分配一些內存(我們可以從IDE中的編譯器設置更改設置)而OS是給你內存的,它取決於在系統上的許多可用內存和許多其他東西上,當我們聲明一個他們復制的變量(ref as formals)這些變量被推送到堆棧時,它們會分配到堆棧內存它們遵循一些命名約定默認情況下它在Visual Studio中的CDECL ex:中綴符號:c = a + b; 堆棧推送從右到左推動,b到堆棧,操作員,堆棧和那些i,ec堆疊的結果。 在預修復符號中:= + cab這里將所有變量推送到堆棧1(從右到左),然后進行操作。 編譯器分配的內存是固定的。 因此,假設我們的應用程序分配了1MB的內存,假設變量使用了700kb的內存(除非動態分配,否則所有局部變量都被推送到堆棧中),因此剩余的324kb內存被分配給堆。 並且這個堆棧的生命周期較短,當函數的范圍結束時,這些堆棧被清除。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM