堆疊

堆疊

在計算機領域,堆疊是一個不容忽視的概念,是一種數據結構。堆疊都是一種數據項按序排列的數據結構,只能在一端(稱為棧頂(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),堆上尋找對象的任務交給句柄,而棧中由棧指針管理

相關詞條

熱門詞條

聯絡我們