面試官問:C#的值類型和引用類型的存儲結構?
.NET大牛之路 ? 王亮@精致碼農 ? 2021.08.21
我們知道,程序運行時,它的數(shù)據(jù)是存儲在內存中的。當我們的程序訪問某個變量時,編譯器負責把人們可以理解的變量名轉換為處理器可以理解的內存地址,處理器通過內存地址找到內存中的存儲單元,然后讀取其中的數(shù)據(jù)。
運行中的 .NET 應用程序使用兩個區(qū)域來存儲數(shù)據(jù):棧和托管堆,其中托管堆簡稱為堆。
我們也知道,C# 中的數(shù)據(jù)類型分為兩種:值類型和引用類型。值類型包含所有的數(shù)字類型(如 byte、int、long、double 等)、布爾型(bool)、字符(char)、結構(struct)和枚舉(enum),其它的都是引用類型(如類、接口、數(shù)組等)。
數(shù)據(jù)的類型不僅決定了數(shù)據(jù)存儲需要的內存大小,還決定了對象在內存中存儲的位置(棧或堆)。理解值類型和引用類型的特點和它們在內存中的存儲結構,就能了解它們是如何以及何時進行內存分配和回收的,這有助于幫助我們編寫更高性能的應用程序。
1棧與值類型
值類型變量的值是存儲在棧中的。學過數(shù)據(jù)結構我們都知道,棧是一個后進先出(LIFO)的數(shù)據(jù)結構。這種數(shù)據(jù)結構的主要特征是,數(shù)據(jù)只能從棧的頂端插入和刪除。把數(shù)據(jù)放入棧頂稱為入棧,從棧頂刪除數(shù)據(jù)稱為出棧。用圖表示如下:

棧在內存中可以理解為上圖所示的一個個連續(xù)的存儲單元。棧除了存儲值類型的變量,還存儲傳遞給方法的值類型的參數(shù),以及程序當前的執(zhí)行環(huán)境等。
我們不需要顯式地對棧做任何操作,棧中數(shù)據(jù)的生命周期由 CLR 根據(jù)其作用域直接處理的。
考慮如下代碼:
{
int a = 1; // a 的作用域開始
// ...
{
int b = 2; // b 的作用域開始
// ...
} // b 的作用域結束
} // a 的作用域結束作用域的生命周期和棧的后進先出邏輯總是一致的。隨著代碼的執(zhí)行,程序先進入變量 a 的作用域,再進入 b 的作用域。對應的,變量 a 的值先入棧,b 的值后入棧。b 的作用域先結束,它的值先出棧被銷毀,其次是 a 的值出棧被銷毀。
2堆與引用類型
托管堆是一塊內存區(qū)域,與棧不同的是,堆中的存儲單元能能夠以任意順序存入和移除。
對于 .NET 程序,堆中的數(shù)據(jù)是由 CLR 托管。CLR 中的 GC(垃圾回收器)判斷程序將不會再訪問某數(shù)據(jù)項時,會自動銷毀無主的堆對象。用圖表示如下:

引用類型對象的數(shù)據(jù)存儲在堆中,同時也會在棧中存儲一個指向堆中實際數(shù)據(jù)的引用,用圖表示如下:

值得注意的是,對于引用類型的任何對象,其實例所有成員的數(shù)據(jù)都存放在堆中,無論它是值類型還是引用類型。
3小結
從數(shù)據(jù)存儲結構的特點來總結一下值類型與引用類型的本質區(qū)別。
第一點不同是分配內存的時機及可變性。引用類型的對象從聲明開始便分配內存,聲明時它在內存中占用的存儲單元就固定了,銷毀前不會再發(fā)生增加或減少容量,賦值只是往已分配的存儲單元中寫入數(shù)據(jù);引用類型是在真正賦值或初始化時才分配內存,而且所分配的內存大小后面可能會根據(jù)需要動態(tài)發(fā)生變化(字符串類型除外)。
第二點不同是它們的存儲位置。值類型只存儲在棧中,只在棧頂進行插入和刪除,遵循后進先出原則;引用類型分兩塊存儲,在堆中存儲實際的數(shù)據(jù),在棧中存儲指向數(shù)據(jù)的引用。
加入我們,一起踏上.NET大牛成長之路↓
