堆疊

堆疊

在計算機領域,堆疊是一個不容忽視的概念,是一種數據結構。堆疊都是一種數據項按序排列的數據結構,只能在一端(稱為棧頂(top))對數據項進行插入和刪除。在單片機套用中,堆疊是個特殊的存儲區,主要功能是暫時存放數據和地址,通常用來保護斷點和現場。要點對比:指令佇列,先進先出(FIFO—first in first out)。堆疊,先進後出 (FILO—First-In/Last-Out)。

基本介紹

  • 中文名:堆疊
  • 外文名: stack
  • 領域:計算機
  • 定義數據結構
  • 功能:對數據項進行插入和刪除
簡介,對比分析,堆疊空間分配,堆疊快取方式,堆疊數據結構區別,區別介紹,java,C/C++,理論知識,申請方式,申請回響,申請限制,效率比較,存儲內容,存取比較,小結,主要分別,補充說明,

簡介

單片機套用中,堆疊是個特殊存儲區,堆疊屬於RAM空間的一部分,堆疊用於函式調用、中斷切換時保存和恢復現場數據。堆疊中的物體具有一個特性:第一個放入堆疊中的物體總是被最後拿出來, 這個特性通常稱為先進後出 (FILO—First-In/Last-Out)。 堆疊中定義了一些操作, 兩個最重要的是PUSH和POP。 PUSH(入棧)操作:堆疊指針(SP)加1,然後在堆疊的頂部加入一 個元素。POP(出棧)操作相反,出棧則先將SP所指示的內部ram單元中內容送入直接地址定址的單元中(目的位置),然後再將堆疊指針(SP)減1.。這兩種操作實現了數據項的插入和刪除。

對比分析

堆疊空間分配

堆(作業系統):由作業系統自動分配釋放 ,存放函式的參數值局部變數的值等。其操作方式類似於數據結構中的棧。
棧(作業系統): 一般由程式設計師分配釋放, 若程式設計師不釋放,程式結束時可能由OS回收,分配方式倒是類似於鍊表。

堆疊快取方式

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

堆疊數據結構區別

堆(數據結構):堆可以被看成是一棵樹,如:堆排序。
棧(數據結構):一種先進後出的數據結構。
例如:順序棧AStack的類定義
template < class T >
class AStack {
private:
int size ; // 數組的規模
T * stackArray ; // 存放堆疊元素的數組
int top ; // 棧頂所在數組元素的下標
public:
AStack ( int MaxStackSize ) // 構造函式
{ size = MaxStackSize ; stackArray = new T [MaxStackSize] ; top = -1 ; }
~AStack ( ) { delete [ ] stackArray ; } // 析構函式
bool Push ( const T& item ) ; // 向棧頂壓入一個元素
bool Pop ( T & item ) ; // 從棧頂彈出一個元素
bool Peek ( T & item ) const ; // 存取棧頂元素
int IsEmpty ( void ) const { return top = = -1 ; }
// 檢測棧是否為空
int IsFull ( void ) const { return top   size-1 ; }
// 檢測棧是否為滿
void clear ( void ) { top-1 ; } // 清空棧
} ;

區別介紹

java

1. 棧(stack)與堆(heap)都是Java用來在Ram中存放數據的地方。與C++不同,Java自動管理棧和堆,程式設計師不能直接地設定棧或堆。
2. 棧的優勢是,存取速度比堆要快,僅次於直接位於CPU中的暫存器。但缺點是,存在棧中的數據大小與生存期必須是確定的,缺乏靈活性。另外,棧數據在多個執行緒或者多個棧之間是不可以共享的,但是在棧內部多個值相等的變數是可以指向一個地址的,詳見第3點。堆的優勢是可以動態地分配記憶體大小,生存期也不必事先告訴編譯器,Java的垃圾收集器會自動收走這些不再使用的數據。但缺點是,由於要在運行時動態分配記憶體,存取速度較慢。
3.Java中的數據類型有兩種。
一種是基本類型(primitivetypes), 共有8種,即int,short, long, byte, float, double, boolean, char(注意,並沒有string的基本類型)。這種類型的定義是通過諸如int a= 3; long b = 255L;的形式來定義的,稱為自動變數。值得注意的是,自動變數存的是字面值,不是類的實例,即不是類的引用,這裡並沒有類的存在。如int a= 3; 這裡的a是一個指向int類型的引用,指向3這個字面值。這些字面值的數據,由於大小可知,生存期可知(這些字面值固定定義在某個程式塊裡面,程式塊退出後,欄位值就消失了),出於追求速度的原因,就存在於棧中。
另外,棧有一個很重要的特殊性,就是存在棧中的數據可以共享。假設我們同時定義:
int a=3;int b=3;
編譯器先處理int a= 3;首先它會在棧中創建一個變數為a的記憶體空間,然後查找有沒有字面值為3的地址,沒找到,就開闢一個存放3這個字面值的地址,然後將a指向3的地址。接著處理int b= 3;在創建完b的引用變數後,由於在棧中已經有3這個字面值,便將b直接指向3的地址。這樣,就出現了a與b同時均指向3的情況。
特別注意的是,這種字面值的引用與類對象的引用不同。假定兩個類對象的引用同時指向一個對象,如果一個對象引用變數修改了這個對象的內部狀態,那么另一個對象引用變數也即刻反映出這個變化。相反,通過字面值的引用來修改其值,不會導致另一個指向此字面值的引用的值也跟著改變的情況。如上例,我們定義完a與b的值後,再令a=4;那么,b不會等於4,還是等於3。在編譯器內部,遇到a=4;時,它就會重新搜尋棧中是否有4的字面值,如果沒有,重新開闢地址存放4的值;如果已經有了,則直接將a指向這個地址。因此a值的改變不會影響到b的值。
另一種是包裝類數據,【如Integer,String, Double等將相應的基本數據類型包裝起來的類。這些類數據全部存在於【堆】中】,Java用new()語句來顯示地告訴編譯器,在運行時才根據需要動態創建,因此比較靈活,但缺點是要占用更多的時間。 4.String是一個特殊的包裝類數據。即可以用String str = new String("abc");的形式來創建,也可以用String str = "abc";的形式來創建(作為對比,在JDK 5.0之前,你從未見過Integer i = 3;的表達式,因為類與字面值是不能通用的,除了String。而在JDK5.0中,這種表達式是可以的!因為編譯器在後台進行Integer i = new Integer(3)的轉換)。前者是規範的類的創建過程,即在Java中,一切都是對象,而對象是類的實例,全部通過new()的形式來創建。Java中的有些類,如DateFormat類,可以通過該類的getInstance()方法來返回一個新創建的類,似乎違反了此原則。其實不然。該類運用了單例模式來返回類的實例,只不過這個實例是在該類內部通過new()來創建的,而getInstance()向外部隱藏了此細節。那為什麼在String str = "abc";中,並沒有通過new()來創建實例,是不是違反了上述原則?其實沒有。
4. 關於String str = "abc"的內部工作。Java內部將此語句轉化為以下幾個步驟:【String str = "abc",String str不要連著】
(1)先定義一個名為str的對String類的對象引用變數:String str;
(2)【在【棧】中查找有沒有存放值為"abc"的地址,如果沒有,則開闢一個存放字面值為"abc"的地址,接著創建一個新的String類的對象o,並將o的字元串值指向這個地址,而且在棧中這個地址旁邊記下這個引用的對象o。如果已經有了值為"abc"的地址,則查找對象o,並返回o的地址。】【上文說數據時存放在堆中,此文說數據存放在棧中】[因為此處不是通過new()創建的啊]
(3)將str指向對象o的地址。
值得注意的是,一般String類中字元串值都是直接存值的。但像String str = "abc";這種場合下,其字元串值卻是保存了一個指向存在棧中數據的引用!
為了更好地說明這個問題,我們可以通過以下的幾個代碼進行驗證。
複製內容到剪貼簿代碼:
String str1="abc";String str2="abc";System.out.println(str1==str2);//true
注意,我們這裡並不用str1.equals(str2);的方式,因為這將比較兩個字元串的值是否相等。==號,根據JDK的說明,只有在兩個引用都指向了同一個對象時才返回真值。而我們在這裡要看的是,str1與str2是否都指向了同一個對象。
結果說明,JVM創建了兩個引用str1和str2,但只創建了一個對象,而且兩個引用都指向了這個對象。
我們再來更進一步,將以上代碼改成:
複製內容到剪貼簿代碼:
String str1="abc";String str2="abc";str1="bcd";System.out.println(str1+","+str2);//bcd,abcSystem.out.println(str1==str2);//false
這就是說,賦值的變化導致了類對象引用的變化,str1指向了另外一個新對象!而str2仍舊指向原來的對象。上例中,當我們將str1的值改為"bcd"時,JVM發現在棧中沒有存放該值的地址,便開闢了這個地址,並創建了一個新的對象,其字元串的值指向這個地址。
事實上,String類被設計成為不可改變(immutable)的類。如果你要改變其值,可以,但JVM在運行時根據新值悄悄創建了一個新對象,然後將這個對象的地址返回給原來類的引用。這個創建過程雖說是完全自動進行的,但它畢竟占用了更多的時間。在對時間要求比較敏感的環境中,會帶有一定的不良影響。
再修改原來代碼:
複製內容到剪貼簿代碼:
String str1="abc";String str2="abc";str1="bcd";String str3=str1;System.out.println(str3);//bcdString str4="bcd";System.out.println(str1==str4);//true
我們再接著看以下的代碼。
複製內容到剪貼簿代碼:
String str1 = new String("abc");
String str2 = "abc";
System.out.println(str1==str2); //false
String str1 = "abc";
String str2 = new String("abc");
System.out.println(str1==str2); //false
創建了兩個引用。創建了兩個對象。兩個引用分別指向不同的兩個對象。
以上兩段代碼說明,只要是用new()來新建對象的,都會在堆中創建,而且其字元串是單獨存值的,即使與棧中的數據相同,也不會與棧中的數據共享。
5. 數據類型包裝類的值不可修改。不僅僅是String類的值不可修改,所有的數據類型包裝類都不能更改其內部的值。
6. 結論與建議:
(1)我們在使用諸如String str = "abc";的格式定義類時,總是想當然地認為,我們創建了String類的對象str。擔心陷阱!對象可能並沒有被創建!唯一可以肯定的是,指向String類的引用被創建了。至於這個引用到底是否指向了一個新的對象,必須根據上下文來考慮,除非你通過new()方法來顯要地創建一個新的對象。因此,更為準確的說法是,我們創建了一個指向String類的對象的引用變數str,這個對象引用變數指向了某個值為"abc"的String類。清醒地認識到這一點對排除程式中難以發現的bug是很有幫助的。
(2)使用String str = "abc";的方式,可以在一定程度上提高程式的運行速度,因為JVM會自動根據棧中數據的實際情況來決定是否有必要創建新對象。而對於Stringstr = new String("abc");的代碼,則一概在堆中創建新對象,而不管其字元串值是否相等,是否有必要創建新對象,從而加重了程式的負擔。這個思想應該是享元模式的思想,但JDK的內部在這裡實現是否套用了這個模式,不得而知。
(3)當比較包裝類裡面的數值是否相等時,用equals()方法;當測試兩個包裝類的引用是否指向同一個對象時,用==。
(4)由於String類的immutable性質,當String變數需要經常變換其值時,應該考慮使用StringBuffer類,以提高程式效率

C/C++

一個由C/C++編譯的程式占用的記憶體分為以下幾個部分
1、棧區(stack)— 由編譯器自動分配釋放 ,存放函式的參數名,局部變數的名等。其操作方式類似於數據結構中的棧。
2、堆區(heap)— 由程式設計師分配釋放, 若程式設計師不釋放,程式結束時可能由OS回收。注意它與數據結構中的堆是兩回事,分配方式倒是類似於鍊表
3、靜態區(static)—全局變數和局部靜態變數的存儲是放在一塊的。程式結束後由系統釋放。
4、文字常量區—常量字元串就是放在這裡的,程式結束後由系統釋放 。
5、程式代碼區— 存放函式體二進制代碼
變數的存儲方式
存儲描述
持續性
作用域
連結性
如何聲明
自動
自動
代碼塊
在代碼塊中
暫存器
自動
代碼塊
在代碼塊中,使用關鍵字 register
靜態,無連結性
靜態
代碼塊
在代碼塊中,使用關鍵字 static
靜態,外部連結性
靜態
檔案
外部
不在任何函式內
靜態,內部連結性
靜態
檔案
內部
不在任何函式內,使用關鍵字 static
首先,定義靜態變數時如果沒有初始化編譯器會自動初始化為0.。接下來,如果是使用常量表達式初始化了變數,則編譯器僅根據檔案內容(包括被包含的頭檔案)就可以計算表達式,編譯器將執行常量表達式初始化。必要時,編譯器將執行簡單計算。如果沒有足夠的信息,變數將被動態初始化。請看一下代碼:
int global_1=1000;//靜態變數外部連結性常量表達式初始化int global_2;//靜態變數外部連結性零初始化static int one_file_1=1000;//靜態變數內部連結性常量表達式初始化static int one_file_2;//靜態變數內部連結性零初始化int main(){static int count_1=1000;//靜態變數無連結性常量表達式初始化static int count_2;//靜態變數無連結性零初始化return 0;}
所有的靜態持續變數都有下述初始化特徵:未被初始化的靜態變數的所有位都被設為0。這種變數被稱為零初始化。以上代碼說明關鍵字static的兩種用法,但含義有些不同:用於局部聲明,以指出變數是無連結性的靜態變數時,static表示的是存儲持續性;而用於代碼塊外聲明時,static表示內部連結性,而變數已經是靜態持續性了。有人稱之為關鍵字重載,即關鍵字的含義取決於上下文。

理論知識

申請方式

stack:
由系統自動分配。 例如,聲明在函式中一個局部變數int b; 系統自動在棧中為b開闢空間
heap:
需要程式設計師自己申請,並指明大小,在c中malloc函式
如p1 = (char *)malloc(10);
在C++中用new運算符
如p2 = new char[10];//(char *)malloc(10);
但是注意p1、p2本身是在棧中的。

申請回響

棧:只要棧的剩餘空間大於所申請空間,系統將為程式提供記憶體,否則將報異常提示棧溢出
堆:首先應該知道作業系統有一個記錄空閒記憶體地址鍊表,當系統收到程式的申請時,會遍歷該鍊表,尋 找第一個空間大於所申請空間的堆結點,然後將結點從空閒結點鍊表中刪除,並將該結點的空間分配給程式,另外,對於大多數系統,會在這塊記憶體空間中的首地址處記錄本次分配的大小,這樣,代碼中的delete語句才能正確的釋放本記憶體空間。另外,由於找到的堆結點的大小不一定正好等於申請的大小,系統會自動的將多餘的那部分重新放入空閒鍊表中。

申請限制

棧:在Windows下,是向低地址擴展的數據結構,是一塊連續的記憶體的區域。這句話的意思是棧頂的地址和棧的最大容量是系統預先規定好的,在 WINDOWS下,棧的大小是2M(也有的說是1M,總之是一個編譯時就確定的常數),如果申請的空間超過棧的剩餘空間時,將提示overflow。因此,能從棧獲得的空間較小。
堆:堆是向高地址擴展的數據結構,是不連續的記憶體區域。這是由於系統是用鍊表來存儲的空閒記憶體地址的,自然是不連續的,而鍊表的遍歷方向是由低地址向高地址。堆的大小受限於計算機系統中有效的虛擬記憶體。由此可見,堆獲得的空間比較靈活,也比較大。

效率比較

棧由系統自動分配,速度較快。但程式設計師是無法控制的。
堆是由new分配的記憶體,一般速度比較慢,而且容易產生記憶體碎片,不過用起來最方便.
另外,在WINDOWS下,最好的方式是用VirtualAlloc分配記憶體,他不是在堆,也不是在棧,而是直接在進程的地址空間中保留一塊記憶體,雖然用起來最不方便。但是速度快,也最靈活

存儲內容

棧: 在函式調用時,在大多數的C編譯器中,參數是由右往左入棧的,然後是函式中的局部變數。注意靜態變數是不入棧的。
當本次函式調用結束後,局部變數先出棧,然後是參數,最後棧頂指針指向函式的返回地址,也就是主函式中的下一條指令的地址,程式由該點繼續運行。
堆:一般是在堆的頭部用一個位元組存放堆的大小。堆中的具體內容由程式設計師安排。

存取比較

char s1[] = "aaaaaaaaaaaaaaa";
char *s2 = "bbbbbbbbbbbbbbbbb";
aaaaaaaaaaa是在運行時刻賦值的;
而bbbbbbbbbbb是在編譯時就確定的;
但是,在以後的存取中,在棧上的數組指針所指向的字元串(例如堆)快。
比如:
#includevoid main(){char a = 1;char c[] = "1234567890";char *p ="1234567890";a = c[1];a = p[1];return;}
對應的彙編代碼
10: a = c[1];
00401067 8A 4D F1 mov cl,byte ptr [ebp-0Fh]
0040106A 88 4D FC mov byte ptr [ebp-4],cl
11: a = p[1];
0040106D 8B 55 EC mov edx,dword ptr [ebp-14h]
00401070 8A 42 01 mov al,byte ptr [edx+1]
00401073 88 45 FC mov byte ptr [ebp-4],al
第一種在讀取時直接就把字元串中的元素讀到暫存器cl中,而第二種則要先把指針值讀到edx中,再根據edx讀取字元,顯然慢了。

小結

堆和棧的區別可以用如下的比喻來看出:
使用棧就象我們去飯館裡吃飯,只管點菜(發出申請)、付錢、和吃(使用),吃飽了就走,不必理會切菜、洗菜等準備工作和洗碗、刷鍋等掃尾工作,他的好處是快捷,但是自由度小。
使用堆就象是自己動手做喜歡吃的菜餚,比較麻煩,但是比較符合自己的口味,而且自由度大。

主要分別

作業系統方面的堆和棧,如上面說的那些。
還有就是數據結構方面的堆和棧,這些都是不同的概念。這裡的堆實際上指的就是(滿足堆性質的)優先佇列的一種數據結構,第1個元素有最高的優先權;棧實際上就是滿足後進先出的性質的數學或數據結構。
雖然堆疊,堆疊的說法是連起來叫,但是他們還是有很大區別的,連著叫只是由於歷史的原因。
堆與棧的分布堆與棧的分布

補充說明

堆疊是一種存儲部件,即數據的寫入跟讀出不需要提供地址,而是根據寫入的順序決定讀出的順序。
形象來說,棧就是一條流水線,而流水線中加工的就是方法的主要程式,在分配棧時,由於程式是自上而下順序執行,就將程式指令一條一條壓入棧中,就像流水線一樣。而堆上站著的就是工作人員,他們加工流水線中的商品,由程式設計師分配:何時加工,如何加工。而我們通常使用new運算符為對象在堆上分配記憶體(C#,Java),堆上尋找對象的任務交給句柄,而棧中由棧指針管理

相關詞條

熱門詞條

聯絡我們