堆疊,堆積,指針介紹

我被程式碼海扁 @codebeatme
閱讀 9:46·字數 2932·發佈 

先決條件
閱讀本節的先決條件是對值,資料型別,變數等概念有所掌握,你可以檢視值,資料型別介紹變數,常數介紹,變數與常數的區別兩節來了解他們。

請註意,本節講述的並非資料結構中的堆疊和堆積。

簡介

堆疊(Stack)和堆積(Heap)是兩種不同的儲存空間分配方案,其執行機製在不同的作業系統和語言執行環境中有所不同。

堆疊

堆疊(Stack)是一種高效連續的資料儲存空間分配方案,資料個體之間緊密相連,當每個資料個體的大小和排列順序已知時,將可以算出所有個體在堆疊中的相對位址。在程式的執行過程中,根據堆疊的實際位址和資料個體的相對位址,存取資料將是簡便快捷的。

除了上述特征,堆疊還遵循後進先出的原則,即最後進堆疊的資料會最先出堆疊,這種設計有利於巢狀呼叫的場景,當一個新的場景開始時,他需要的資料會進堆疊,當新場景結束時,他需要的資料會出堆疊,這相當於還原了上一個場景所需的資料,如果上一個場景存在的話。

堆疊和變數

堆疊是一個底層概念,與程式碼中書寫的變數沒有直接關聯,但變數對應的值是應該儲存在堆疊中的,以獲得較高的存取速度,要完成這一目標,值或者值的資料型別所占用的儲存空間大小應該是已知和確定的,這是非常重要的一點。

占用的儲存空間大小不確定
並非所有儲存空間大小都是可以確定的,原因可能是以下兩種,變數能夠接受的值的實際資料型別不確定,比如,某一個變數的型別宣告為類別Plant,但其被賦予的值卻可能是類別Tree或類別Flower的執行個體,或者,變數的型別本身占用的空間不確定,比如,變數的型別是長度隨時可變的陣列。

對於空間大小的不確定性,堆疊是難於處理的,比如,變數被賦予了比之前占用更多空間的新值,如何拓展空間來儲存該新值?無論采取什麽方案,有兩種結果是極可能出現的,需要移動堆疊內的部分資料,導致寫入變數的效率降低,一個資料的儲存空間被分為多塊,導致讀取變數的效率降低。這些情況並不符合堆疊的設計初衷,他們應該使用另一套方案,那就是堆積。

堆疊和函式

基於後進先出的特征,函式與堆疊是契合的,當出現一個新的函式呼叫時,堆疊會為該函式分配空間以讀取及寫入相關資料,這些資料可能是參數的值,區域變數的值,或傳回值。當函式結束時,堆疊會清除之前分配的空間,並還原函式被呼叫前的狀況,以讓函式呼叫方恢復執行。

堆疊和執行緒

每一個執行緒都維護了自己的堆疊,並且不與其他執行緒共用,因為你不能接受一個執行緒正在使用堆疊中的資料,而另一個執行緒卻要求資料進堆疊或出堆疊,當所有執行緒共用同一個堆疊時,這會是經常性的問題。

堆積

堆積(Heap)是一種非連續的資料儲存空間分配方案,每個資料個體不必是緊密相連的,他們之間可能存在一些閑置空間。因此,對於分配新的空間,堆積的效率不及堆疊。

堆積片段

當堆積中的資料被清除後,其對應的儲存空間將處於閑置狀態,並可用於新的分配要求,如果其大小小於要求所需的大小,那麽這些閑置空間可能會一直處於閑置狀態,成為堆積中的片段,無法被利用。

也許你已經想到了,通過簡單的合併相鄰的閑置空間,堆積片段的問題會得到緩解,但這一般不會涉及移動堆積內的資料,因為會付出較大成本並具有一定風險,當資料的位置改變後,你需要保證該資料的使用方依然可以正確存取他,這樣的做法雖然在理論上可行,但現實依然要考慮效率和空間之間的平衡。

堆積和參考

鑒於堆積的空間分配方式,堆積中資料的位址是無法被預知的,當某個資料對應的空間被分配後,該空間的位址或存取權杖應該被保留,以便再次存取,這些位址或存取權杖即是對堆積資料的參考。

對於變數來說,如果他的值被存放堆積中,那麽對應的參考應該存放在堆疊中,他是一個可以想到且合理的位置。讀取該變數的過程將是,首先取得其在堆疊中對應的資料,也就是一個參考,然後使用參考取得堆積中的資料,也就是變數希望表示的實際值。

什麽是指針?
指針(Pointer)是一種簡單但並非唯一的參考實作,也由於他的簡單,並沒有太多規則來約束其行為,一些錯誤的使用可能導致程式或系統當機,指針存取了不應該存取的資料。這裏有兩個與指針有關的概念,取值和取址。取值是指取得指針對應的實際值,取址是指根據實際值產生一個對應的指針。

什麽是實值型別和參考型別?
如果某種資料型別的值應該儲存在堆疊中,那麽該資料型別為實值型別,如果值應該儲存在堆積中,則該資料型別為參考型別。實值型別變數在堆疊中對應的資料是實際值,參考型別變數在堆疊中對應的資料是實際值的參考。

什麽是傳值和傳址?
傳值是一種預設行為,他將變數在堆疊中的資料賦予另一個變數,作為其堆疊中的資料,無論這些資料是實際值還是參考。需要指出,傳值中的“值”一詞不應理解為實際值,雖然他有可能是。

傳址類似於別名,變數們看上去好像共用了堆疊中的同一個儲存空間,不過,不要認為這樣的事情一定是現實,作為別名的變數可能在自己的堆疊儲存空間中存放了一個位址,以指向堆疊中的另一個儲存空間,也許在具體實作上存在不同,但最終效果是讀取及寫入任意一個變數,其余的變數均會受到影響。

在下面的 C# 程式碼中,將變數num1以傳址的方式指派給變數num2,然後將num2指派為200,最後的輸出結果顯示變數num1的值也是200

*.cs
int num1 = 100;
// 以傳址的方式,將 num1 指派給 num2
ref int num2 = ref num1;
// 對 num2 指派,等同於對 num1 指派
num2 = 200;
Console.WriteLine($"num1={num1}");
num1=200

堆積和行程

與堆疊不同,堆積通常在行程中維護,並在所有的執行緒中共用。

堆積記憶體回收

通過維護資料的參考計數,可以知道堆積中的某個資料有多少參考,這些參考可能來自同一個或不同的執行緒。至於參考計數為零的資料,將被記憶體回收機製以某種策略清理,由於記憶體回收可能是不定時的,不再被使用的資料有機會存活更長的時間。

裝箱,取消裝箱

裝箱是指將堆疊中儲存的值,轉換為一個等價的儲存在堆積中的值。取消裝箱則是指將堆積中儲存的值,轉換為一個等價的儲存在堆疊中的值。

為何有裝箱和取消裝箱?
對於將所有資料型別都視為類別的語言,如果完全依照類別的特點,那麽所有的變數都不能是實值型別變數,因為在變數宣告為某種型別後,其儲存的實際值卻可能是另外的某種型別,這導致堆疊無法確定應該為變數分配多大的儲存空間。

這本身是一個矛盾的問題,既希望將所有資料型別都視為類別,又希望將某些值存放在堆疊中,要實作這一目標,某些資料型別所需的儲存空間必須是確定的,變數在宣告為這些資料型別後,其接受的實際值也只能是這些資料型別,比如,.NET 中的System.Int32

以上的這些情況,導致某些實值型別變數和參考型別變數的相互指派,是被允許且看上去正常的,因為他們衍生自同一個類別。如果這種指派真的發生,那麽必然需要堆疊與堆積中的值的等價轉換操作。

當然,我們不能將裝箱和取消裝箱的原因都歸於類別,因為每種語言都可以有自己的規則。

在下面的 C# 程式碼中,num是實值型別變數,將其指派給參考型別變數something需要裝箱,將something指派給實值型別變數age需要取消裝箱。

*.cs
// num 是實值型別變數
int num = 123;
// something 是參考型別變數,因此需要裝箱
object something = num;
// age 是實值型別變數,因此需要取消裝箱
int age = (int)something;

程式碼

stack_and_heap.cs·codebeatme/programming·GitHub