數據結構的堆和棧的概念


  在數據結構中,棧是一種可以實現“先進后出”(或者稱為“后進先出”)的存儲結構。進棧的順序和出棧的順序是相反的。在實際編程中,可以通過兩種方式來實現:使用數組的形式來實現棧,這種棧也稱為靜態棧;使用鏈表的形式來實現棧,這種棧也稱為動態棧。

  相對於棧的“先進后出”特性,堆則是一種經過排序的樹形數據結構,常用來實現優先隊列等。假設有一個集合 K={k0,k1,…,kn-1},把它的所有元素按完全二叉樹的順序存放在一個數組中,並且滿足:

    

則稱這個集合 K 為最小堆(或者最大堆)。

  由此可見,堆是一種特殊的完全二叉樹。其中,節點是從左到右填滿的,並且最后一層的樹葉都在最左邊(即如果一個節點沒有左兒子,那么它一定沒有右兒子);每個節點的值都小於(或者都大於)其子節點的值。

  堆棧都是一種數據項按序排列的數據結構,只能在一端(稱為棧頂(top))對數據項進行插入和刪除。

  堆:隊列優先,先進先出(FIFO—firstinfirstout)。棧:先進后出(FILO—First-In/Last-Out)。

1、內存分配中的堆和棧

在 C 語言中,內存分配方式有如下三種形式:

  1)從靜態存儲區域分配:它是由編譯器自動分配和釋放的,即內存在程序編譯的時候就已經分配好,這塊內存在程序的整個運行期間都存在,直到整個程序運行結束時才被釋放,如全局變量與 static 變量。

  2)在棧上分配:它也是由編譯器自動分配和釋放的,即在執行函數時,函數內局部變量的存儲單元都可以在棧上創建,函數執行結束時這些存儲單元將被自動釋放。需要注意的是,棧內存分配運算內置於處理器的指令集中,它的運行效率一般很高,但是分配的內存容量有限。

  3)從堆上分配:也被稱為動態內存分配,它是由程序員手動完成申請和釋放的。即程序在運行的時候由程序員使用內存分配函數(如 malloc 函數,new)來申請任意多少的內存,使用完之后再由程序員自己負責使用內存釋放函數(如 free 函數,delete)來釋放內存。也就是說,動態內存的整個生存期是由程序員自己決定的,使用非常靈活。需要注意的是,如果在堆上分配了內存空間,就必須及時釋放它,否則將會導致運行的程序出現內存泄漏等錯誤。

  內存中的棧區主要用於分配局部變量空間,處於相對較高的地址,其棧地址是向下增長的,即是最后入棧的地址最小;而堆區則主要用於分配程序員申請的內存空間,堆地址是向上增長的,即是最后的地址最大。

  棧使用的是一級緩存,他們通常都是被調用時處於存儲空間中,調用完畢立即釋放。堆則是存放在二級緩存中,生命周期由虛擬機的垃圾回收算法來決定(並不是一旦成為孤兒對象就能被回收)。所以調用這些對象的速度要相對來得低一些。

1.1 內存分配中堆和棧的區別

1.1.1 分配與釋放方式

棧內存是由編譯器自動分配與釋放的,它有兩種分配方式:靜態分配和動態分配。

  靜態分配是由編譯器自動完成的,如局部變量的分配(即在一個函數中聲明一個 int 類型的變量i時,編譯器就會自動開辟一塊內存以存放變量 i)。與此同時,其生存周期也只在函數的運行過程中,在運行后就釋放,並不可以再次訪問。

  動態分配由 alloca 函數進行分配,但是棧的動態分配與堆是不同的:它的動態分配是由編譯器進行釋放,無需任何手工實現。值得注意的是,雖然用 alloca 函數可以實現棧內存的動態分配, 但alloca 函數的可移植性很差。

  堆內存則不相同,它完全是由程序員手動申請與釋放的,程序在運行的時候由程序員使用內存分配函數(如 malloc 函數)來申請任意多少的內存,使用完再由程序員自己負責使用內存釋放函數。

  堆上的數據比棧上的危險,很容易造成內存泄漏的問題,對程序而言是致命的。

1.1.2 分配的碎片問題

  對堆來說,頻繁分配和釋放(malloc / free)不同大小的堆空間勢必會造成內存空間的不連續,從而造成大量碎片,導致程序效率降低;而對棧來講,則不會存在這個問題。

1.1.3 分配的效率

  棧是機器系統提供的數據結構,計算機會在底層對棧提供支持。分配專門的寄存器存放棧的地址,壓棧出棧都有專門的執行指令,這就決定了棧的效率比較高。一般而言,只要棧的剩余空間大於所申請空間,系統就將為程序提供內存,否則將報異常提示棧溢出。
  而堆則不同,它是由 C/C++ 函數庫提供的,它的機制也相當復雜。為了分配一塊堆內存,首先應該知道操作系統有一個記錄空閑內存地址的鏈表,當系統收到程序的申請時,會遍歷該鏈表,尋找第一個空間大於所申請空間的堆節點,然后將該節點從空閑節點鏈表中刪除,並將該節點的空間分配給程序。而對於大多數系統,會在這塊內存空間的首地址處記錄本次分配的大小,這樣,代碼中的 delete 語句才能正確釋放本內存空間。另外,由於找到的堆節點的大小不一定正好等於申請的大小,系統會自動將多余的那部分重新放入空閑鏈表中。很顯然,堆的分配效率比棧要低得多。

1.1.4  申請的大小限制

  由於操作系統是用鏈表來存儲空閑內存地址(內存區域不連續)的,同時鏈表的遍歷方向是由低地址向高地址進行的。因此,堆內存的申請大小受限於計算機系統中有效的虛擬內存
  而棧則不同,它是一塊連續的內存區域,其地址的增長方向是向下進行的,向內存地址減小的方向增長。由此可見,棧頂的地址和棧的最大容量一般都是由系統預先規定好的,如果申請的空間超過棧的剩余空間時,將會提示溢出錯誤。由此可見,相對於堆,能夠從棧中獲得的空間相對較小。

1.1.5 存儲的內容

  對棧而言,一般用於存放函數的參數與局部變量等。在函數調用時,第一個進棧的是(主函數中的)調用處的下一條指令(即函數調用語句的下一條可執行語句)的地址,然后是函數的各個參數,在大多數 C 編譯器中,參數是由右往左入棧的,最后是函數中的局部變量(注意 static 變量是不入棧的)。當本次函數調用結束后,遵循“先進后出”(或者稱為“后進先出”)的規則,局部變量先出棧,然后是參數,最后棧頂指針指向最開始保存的地址,也就是主函數中的下一條指令,程序由該點繼續運行。printf函數輸出就是這個原理。

  對堆而言,具體存儲內容由程序員根據需要決定存儲數據。

2、變量的存儲位置和作用域

  全局變量:從靜態存儲區域分配,其作用域是全局作用域,也就是整個程序的生命周期內都可以使用。與此同時,如果程序是由多個源文件構成的,那么全局變量只要在一個文件中定義,就可以在其他所有的文件中使用,但必須在其他文件中通過使用extern關鍵字來聲明該全局變量。

  全局靜態變量:從靜態存儲區域分配,其生命周期也是與整個程序同在的,從程序開始到結束一直起作用。但是與全局變量不同的是,全局靜態變量作用域只在定義它的一個源文件內,其他源文件不能使用。

  局部變量:從棧上分配,其作用域只是在局部函數內,在定義該變量的函數內,只要出了該函數,該局部變量就不再起作用,該變量的生命周期也只是和該函數同在。

  局部靜態變量:從靜態存儲區域分配,其在第一次初始化后就一直存在直到程序結束,該變量的特點是其作用域只在定義它的函數內可見,出了該函數就不可見。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM