前言
作為一名程序員,許多同學會把堆和棧這兩個字,變成一個詞放在一起,其實這是錯誤的。
堆(heap)
堆是一個内存空間,這個内存可以由程序員分配和釋放,當然部分語言自帶 GC( Garbage Collection 垃圾回收),部分堆内存可以由 GC 回收。這裡千萬要注意,這裡說的「堆」和 數據結構 裡面說的「堆」不是同一個概念,大家千萬不要混淆。
堆是程序在運行的時候請求操作系統分配給自己内存。由于從操作系統管理的内存分配,所以在分配和銷毀時都要占用時間,因此用堆的效率相對棧來說略低。但是堆的優點在于,編譯器不必知道要從堆裡分配多少内存空間,也不必知道存儲的數據要在堆裡停留多長的時間,因此用堆保存數據時會得到更大的靈活性。所以為達到這種靈活性,在堆裡分配存儲空間時會花相對更長的時間,這也是效率低于棧的原因。
棧(stack)
棧是由編譯器自動分配和釋放的,存放函數的參數值,局部變量的值等。也請注意,這裡說的「棧」不是 數據結構 中的「棧」,大家千萬不要混淆。這裡請注意,棧是由由系統自動分配。
棧的優勢是存取速度比堆要快,僅次于寄存器,棧數據可以共享。但缺點是,存在棧中的數據大小與生存期必須是确定的,缺乏靈活性。
說的再通俗點:
使用棧就像我們去飯館裡吃飯,隻管點菜(發出申請)、付錢、和吃(使用),吃飽了就走,不必理會切菜、洗菜等準備工作和洗碗、刷鍋等掃尾工作,他的好處是快捷,但是自由度小。
使用堆就像是自己動手做喜歡吃的菜肴,比較麻煩,但是比較符合自己的口味,而且自由度大。
實際應用
看完上面一段文字,大多數同學自然就懂了,而不懂的,還是一臉懵。這些東西我知道了有什麼實際意義?比如棧既然是系統分配和釋放,我幹預不了,我知道它幹什麼?那這裡就以編程語言 C#(C Sharp)來舉例說明。也可以看相應的 LeetBook
堆、棧的實際意義
堆、棧,算是相對底層一些的内容,大部分的語言其實這部分内容都是類似的。如果你不會 C# 語言也能看懂~。
我們都知道,C# 的數據類型有兩大類型 —— 值類型 和 引用類型。大緻結構如下圖:
值類型我們看到包含内置值類型、用戶定義的值類型和枚舉。枚舉就不用說了,内置值類型指的是 int、float、bool、double 等等。
用戶定義的值類型指的就是 struct。引用類型大概我們學 C 語言的時候學到的指針類型(C# 做了封裝,所以我們看不到 int * 這類的代碼,當然可以使用 unsafe 關鍵字,這裡就不多贅述)、接口類型、用戶自定義的類(class),數組等。
類型的含義
這裡為什麼要解釋這兩個大的類型?首先是因為值類型是放在棧上的,值類型變量聲明後,不管是否已經賦值,編譯器為其分配内存。然而引用類型當聲明一個變量時,隻在棧中分配一小片内存用于容納一個地址,而此時并沒有為其分配堆上的内存空間。當使用 new 創建一個類的實例時,這時會分配堆上的空間,并把堆上空間的地址保存到棧上分配的小片空間中。
通俗一點,堆上存放的是我們聲明出來的實例對象,當我們需要訪問這個實例對象時,我們找到它的方法是先找到變量對應在棧上的内存,然後通過棧上存放的數據,我們才能找到其真正的實例在堆的任意位置。
代碼
class A{
public A(int x){
X = x;
}
public int X { get; set; }
}
static void Main(string[] args){
int m = 1;
int n = m;
m = 2;
Console.WriteLine($"m = {m}, n = {n}");
A a = new A(1);
var b = a;
a.X = 2;
Console.WriteLine($"a.X = {a.X}, b.X = {b.X}");
}
我們會發現輸出結果是
m = 2, n = 1
a.X = 2, b.X = 2
我們發現 m 和 n 的值是分别獨立的,而 a 和 b 修改其中一個會修改兩個值。這是因為 m 和 n 都是分配在棧上的,而 a 和 b 雖然也是在棧上,但是其隻是存儲的堆上的一個地址索引,我們不管通過 a 還是 b 索引到的堆上的内存都是同一份。
我們再來看值類型和引用類型,在棧上的數據訪問快,在堆上的數據訪問相對慢,因此,當我們開發過程中,通過需求,比如底層的一些不變數據,完全可以有 struct 來實現,因為其不會為 null,符合值類型的要求,而且我們經常訪問會更快一些。
我們看上圖發現,值類型和引用類型都是繼承自 Object ,也就是說:
Object a = 1;
int b = (int)a;
這兩個都是合法語句。但是這裡面隐藏了一個非常常見的一個現象,那就是裝箱和拆箱。
裝箱實際上是将值類型轉換為引用類型,而拆箱是将引用類型轉換為值類型。再透徹點,實際上裝箱拆箱就是棧内存和堆内存的來回拷貝和賦值,這不僅僅浪費性能,而且還會造成沒必要的 GC。因此,能避免 “裝箱”、“拆箱” 操作的就盡量避免。除上面所說的這些數據類型,還有一個寫代碼必不可少的,那就是函數。
函數是如何調用的?實際上函數調用的參數是通過棧空間來傳遞,在調用過程中會占用線程的棧資源。
當我們使用遞歸算法時,每次雖然調用的是同一個函數,但是會在棧中占用一個空間,隻有走到最後的結束點後函數才能依次退出,而未到達最後的結束點之前,占用的棧空間一直沒有釋放。因此如果遞歸調用次數過多,就可能導緻占用的棧資源超過線程的最大值,從而導緻棧溢出,這也是為什麼一定要盡量少用遞歸的原因。
寫在最後
上面說了很多,側面說明了堆、棧的應用的優缺點,平時應用中需要更加了解我們寫的代碼都“幹了什麼”,這樣才有可能寫出更高效、更可靠的代碼。對于一名程序員來說,不管未來編程語言發展的多麼容易上手,一定不要忘了學習計算機的基礎知識,哪怕是先學會了編程再返回來補習這些知識。祝大家工作順利、評論留言少 BUG !點贊轉發不脫發!
BY /
本文作者:力扣
聲明:本文歸“力扣”版權所有,如需轉載請聯系。
,更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!