原文連結:https://jonskeet.uk/csharp/memory.html
人們在理解值型別和引用型別之間的差異時因為“值型別在棧上分配,引用型別在堆上分配”這句話造成了很多混亂。這完全是不對的,本文試圖澄清這個問題。
變數中有什麼?
理解.NET中記憶體工作方式的關鍵是理解變數是什麼,以及它的值是什麼。在最基本的層面上,變數是變數名和記憶體之間的關聯。變數的值是與之關聯的記憶體中的內容。該值佔用記憶體空間的大小和值的解釋取決於變數的型別 - 這正是值型別和引用型別之間的差異所在。
引用型別變數的值始終是引用或null
。如果是引用,則它必須是與其變數型別相容的物件的引用。例如,以Stream s宣告的變數s的值是null或Stream型別(或其相容型別)例項的引用。引用型別變數所佔記憶體空間的大小是引用的大小,引用的大小在32位模式下固定為4個位元組,在64位模式下固定為8個位元組。
值型別變數的值始終是其物件本身的值。例如,對於給定的結構:
struct PairOfInts { public int a; public int b; }
以PairOfInts pair宣告的變數pair的值是整數對本身,而不是對一對整數的引用。其所佔記憶體空間則是兩個整數的大小,即8個位元組。請注意,值型別變數永遠不能賦值為null - 因為這沒有任何意義,值型別變數不是一個引用。
那麼東西存放在哪裡?
變數的分配位置取決於宣告它的上下文:
- 區域性變數在棧上分配。這包括引用型別變數 - 變數本身位於棧上,其引用的值分配在堆上。方法引數也計為區域性變數,但如果使用ref、out、in修飾符修飾它們,則它們不再是原始型別,而是轉換為託管指標型別(Type &),此時傳遞的是原變數的指標,不再是變數本身。
- 引用型別的物件始終在堆上分配。
- 值型別的物件始終內聯分配。即在方法中宣告的值型別變數在棧上分配,而作為類的例項欄位的值型別變數將在堆上分配。
- 靜態變數在堆上分配,包括引用型別和值型別中宣告的靜態變數。無論建立多少個例項,靜態變數都共享一個記憶體空間。
上述規則有幾個例外:在使用匿名方法時的外部變數和迭代器中的區域性變數會由編譯器優化為其它型別的例項欄位,這些變數會轉移到堆中分配。
舉個例子
上述文字描述可能聽起來有點複雜,但一個完整的例子可以讓事情更清楚一些:
using System; struct PairOfInts { static int counter = 0; public int a; public int b; internal PairOfInts(int x, int y) { A = x; B = y; counter++; } } class Test { PairOfInts pair; string name; Test(PairOfInts p, string s, int x) { pair = p; name = s; pair.a + = x; } static void Main() { PairOfInts z = new PairOfInts(1, 2); Test t1 = new Test(z, "first", 1); Test t2 = new Test(z, "second", 2); Test t3 = null; Test t4 = t1; //XXX } }
讓我們看一下標記“XXX”位置時記憶體中的內容。
- 在棧上分配一個PairOfInts型別的物件,對應變數z。
- 在堆上分配一個Test型別的物件,在棧上分配一個引用指向該物件,對應變數t1。以32位模式舉例,該物件在堆中佔用20個位元組:8個位元組的頭資訊(所有堆物件都有),8個位元組用於儲存PairOfInts例項,4個位元組用於儲存字串引用。
- 在堆上分配一個Test型別的物件,在棧上分配一個引用指向該物件,對應變數t2。該物件與上面的物件非常相似。
- 在棧上分配一個引用,對應變數t3。這個引用是null - 它沒有引用任何物件。
- 在棧上分配一個引用,對應變數t4,並賦值t1引用的物件,此時t1和t4引用堆記憶體中的同一個物件。
- 最後,在堆記憶體中有一個靜態變數PairOfInts.counter。
如果您覺得閱讀本文對您有幫助,請點一下“推薦”按鈕,您的認可是我寫作的最大動力!
作者:Minotauros
出處:https://www.cnblogs.com/minotauros/
本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連線,否則保留追究法律責任的權利。