原文鏈接: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/
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。