.NET 大牛之路 | 018 C# 基礎:值型別和引用型別的儲存結構

我們知道,程式執行時,它的資料是儲存在記憶體中的。當我們的程式訪問某個變數時,編譯器負責把人們可以理解的變數名轉換為處理器可以理解的記憶體地址,處理器透過記憶體地址找到記憶體中的儲存單元,然後讀取其中的資料。

執行中的 。NET 應用程式使用兩個區域來儲存資料:

託管堆

,其中

託管堆

簡稱為

我們也知道,C# 中的資料型別分為兩種:

值型別

引用型別

。值型別包含所有的數字型別(如 byte、int、long、double 等)、布林型(bool)、字元(char)、結構(struct)和列舉(enum),其它的都是引用型別(如類、介面、陣列等)。

資料的型別不僅決定了資料儲存需要的記憶體大小,還決定了物件在記憶體中儲存的位置(棧或堆)。理解值型別和引用型別的特點和它們在記憶體中的儲存結構,就能瞭解它們是如何以及何時進行記憶體分配和回收的,這有助於幫助我們編寫更高效能的應用程式。

棧與值型別

值型別變數的值是儲存在棧中的。學過資料結構我們都知道,棧是一個後進先出(LIFO)的資料結構。這種資料結構的主要特徵是,資料只能從棧的頂端插入和刪除。把資料放入棧頂稱為

入棧

,從棧頂刪除資料稱為

出棧

。用圖表示如下:

.NET 大牛之路 | 018 C# 基礎:值型別和引用型別的儲存結構

棧在記憶體中可以理解為上圖所示的一個個連續的儲存單元。棧除了儲存值型別的變數,還儲存傳遞給方法的值型別的引數,以及程式當前的執行環境等。

我們不需要顯式地對棧做任何操作,棧中資料的生命週期由 CLR 根據其作用域直接處理的。

考慮如下程式碼:

{ int a = 1; // a 的作用域開始 // 。。。 { int b = 2; // b 的作用域開始 // 。。。 } // b 的作用域結束} // a 的作用域結束

作用域的生命週期和棧的後進先出邏輯總是一致的。隨著程式碼的執行,程式先進入變數

a

的作用域,再進入

b

的作用域。對應的,變數

a

的值先入棧,

b

的值後入棧。

b

的作用域先結束,它的值先出棧被銷燬,其次是

a

的值出棧被銷燬。

堆與引用型別

託管堆是一塊記憶體區域,與棧不同的是,堆中的儲存單元能能夠以任意順序存入和移除。

對於 。NET 程式,堆中的資料是由 CLR 託管。CLR 中的 GC(垃圾回收器)判斷程式將不會再訪問某資料項時,會自動銷燬無主的堆物件。用圖表示如下:

.NET 大牛之路 | 018 C# 基礎:值型別和引用型別的儲存結構

引用型別物件的資料儲存在堆中,同時也會在棧中儲存一個指向堆中實際資料的引用,用圖表示如下:

.NET 大牛之路 | 018 C# 基礎:值型別和引用型別的儲存結構

值得注意的是,對於引用型別的任何物件,其例項所有成員的資料都存放在堆中,無論它是值型別還是引用型別。

小結

從資料儲存結構的特點來總結一下值型別與引用型別的本質區別。

第一點不同是分配記憶體的時機及可變性。值型別的物件從宣告開始便分配記憶體,宣告時它在記憶體中佔用的儲存單元就固定了,銷燬前不會再發生增加或減少容量,賦值只是往已分配的儲存單元中寫入資料;引用型別是在真正賦值或初始化時才分配記憶體,而且所分配的記憶體大小後面可能會根據需要動態發生變化(字串型別除外)。

第二點不同是它們的儲存位置。值型別只儲存在棧中,只在棧頂進行插入和刪除,遵循後進先出原則;引用型別分兩塊儲存,在堆中儲存實際的資料,在棧中儲存指向資料的引用。

本文來自『。NET大牛之路』體系專欄的分享,追更完整專欄請私撩我……