1. 程式人生 > >值型別和引用型別的儲存

值型別和引用型別的儲存

值型別變數所佔用的記憶體空間位於執行緒堆疊中,而引用型別變數所引用的物件生存於託管堆中。

以下轉載:

  一、值型別和引用型別變數的儲存
    首先,變數是儲存資訊的基本單元,而對於計算機內部來說,變數就相當於一塊記憶體空間。
    C#中的變數可以劃分為值型別和引用型別兩種:
    值型別:簡單型別、結構型別、列舉型別
    引用型別:類、代表、陣列、介面。
    (一)值型別和引用型別記憶體分配
    值型別是在棧中操作,而引用型別則在堆中分配儲存單元。棧在編譯的時候就分配好記憶體空間,在程式碼中有棧的明確定義,而堆是程式執行中動態分配的記憶體空間,可以根據程式的執行情況動態地分配記憶體的大小。因此,值型別總是在記憶體中佔用一個預定義的位元組數(比如,int佔用4個位元組,即32位)。當宣告一個值型別變數時,會在棧中自動分配此變數型別佔用的記憶體空間,並存儲這個變數所包含的值。.NET會自動維護一個棧指標,它包含棧中下一個可用記憶體空間的地址。棧是先入後出的,棧中最上面的變數總是比下面的變數先離開作用域。當一個變數離開作用域時,棧指標向下移動被釋放變數所佔用的位元組數,仍指向下一個可用地址。注意,值型別的變數在使用時必須初始化.
    而引用型別的變數則在棧中分配一個記憶體空間,這個記憶體空間包含的是對另一個記憶體位置的引用,這個位置是託管堆中的一個地址,即存放此變數實際值的地方。.NET也自動維護一個堆指標,它包含堆中下一個可用記憶體空間的地址,但堆不是先入後出的,堆中的物件不人在程式的一個預定義點離開作用域,為了在不使用堆中分存的記憶體時將它釋放,.NET將定期執行垃圾收集。垃圾收集器遞迴地檢查應用程式中所有的物件引用,當發現引用不再有效的物件使用的記憶體無法從程式中訪問時,該記憶體就可以回收(除了fixed關鍵字固定在記憶體中的物件外)。(垃圾收集器原理?)
    但值型別在棧上分配記憶體,而引用型別在託管堆上分配記憶體,卻只是一種籠統的說法。更詳細準確地描述是:
    1、對於值型別的例項,如果做為方法中的區域性變數,則被建立線上程棧上;如果該例項做為型別的成員,則作為型別成員的一部分,連同其他型別欄位存放在託管堆上,
    2、引用型別的例項建立在託管堆上,如果其位元組小於85000byte,則直接建立在託管堆上,否則建立在LOH(Large Objet Heal)上。
    比如一下程式碼段:
         
public class Test
    {
        private int i;    //作為Test例項的一部分,與Test的例項一起被建立在GC堆上
        public Test()
        {
            int j = 0;     //作為區域性實量,j的例項被建立在執行這段程式碼的執行緒棧上
        }
    }
         
    (二)巢狀結構的記憶體分配
    所謂巢狀結構,就是引用型別中巢狀有值型別,或值型別中巢狀有引用型別。
    引用型別巢狀值型別是最常見的,上面的例子就是典型的例子,此時值型別是內聯在引用型別中。
    值型別巢狀引用型別時,該引用型別作為值型別成員的變數,將在堆疊上保留關引用型別的引用,但引用型別還是要在堆中分配記憶體的。
    (三)關於陣列記憶體的分配
    考慮當陣列成員是值型別和引用型別時的情形:
    成員是值型別:比如int[] arr = new int[5]。arr將儲存一個指向託管堆中4*5byte(int佔用4位元組)的地址的引用,同時將所有元素賦值為0;
    引用型別:myClass[] arr = new myClass[5]。arr線上程的堆疊中建立一個指向託管堆的引用。所有元素被置為null。
   
二、值型別和引用型別在傳遞引數時的影響
    由於值型別直接將它們的資料存放在棧中,當一個值型別的引數傳遞給一個方法時,該值的一個新的拷貝被建立並被傳遞,對引數所做的任何修改都不會導致傳遞給方法的變數被修改。而引用型別它只是包含引用,不包含實際的值,所以當方法體內參數所做的任何修改都將影響傳遞給方法呼叫的引用型別的變數。
    下面程式證明了這一點:
         
class Class1
    {
        /// <summary>
        /// 應用程式的主入口點。
        /// </summary>
        [STAThread]
        static void Main(string[] args)
        {
            int i = 0;
            int[] intArr = new int[5];

            Class1.SetValues(i,intArr);
            //輸出的結果將是:i=0,intArr[0]=10
            Console.WriteLine("i={0},intArr[0]={1}",i,intArr[0]);
            Console.Read();

        }

        public static void SetValues(int i,int[] intArr)
        {
            i = 10;
            for (int j = 0; j < intArr.Length; j++)
            {
                intArr[j] = i;
            }
        }
    }
三、裝箱和拆箱
    裝箱是將一個值型別轉換為一個物件型別(object),而拆箱則是將一個物件型別顯式轉換為一個值型別。對於裝箱而言,它是將被裝箱的值型別複製一個副本來轉換,而對於拆箱而言,需要注意型別的相容性,比如,不能將一個值為“a”的object型別轉換為int的型別。
    可以用以下程式來說明:
         
static void Main(string[] args)
        {
            int i = 10;

            //裝箱
            object o = i; //物件型別

            if (o is int)
            {
                //說明已經被裝箱
                Console.WriteLine("i已經被裝箱");
            }

            i = 20; //改變i的值

            //拆箱
            int j = (int)o;

            //輸出的結果是20,10,10
            Console.WriteLine("i={0};o={1};j={2}",i,o,j);
            Console.ReadLine();
        }
                                        
四、關於string
    string是引用型別,但卻與其他引用型別有著一點差別。可以從以下兩個方面來看:
    (1)String類繼承自object類。而不是System.ValueType。
    (2)string本質上是一個char[],而Array是引用型別,同樣是在託管的堆中分配記憶體。
    但String作為引數傳遞時,卻有值型別的特點,當傳一個string型別的變數進入方法內部進行處理後,當離開方法後此變數的值並不會改變。原因是每次修改string的值時,都會建立一個新的物件。比如下面這段程式:
         
class Class1
    {
        /// <summary>
        /// 應用程式的主入口點。
        /// </summary>
        [STAThread]
        static void Main(string[] args)
        {
            string a = "1111";        //a是一個引用,指向string類的一個例項
            string b = a;             //b與a都是同一個物件
           
            //這時候b與a指向的並不是同一樣物件,因為給b賦值後,已經建立了一個新的物件,並將這個新的string物件的引用賦給了b。
            b = "2222";

            //所以a的值不變,輸出a=111.
            Console.WriteLine("a={0}",a);
            Console.ReadLine();
        }
    }
    但要注意,如果按引用傳值時,則會與引用型別的引數一樣,值會發生改變,比如以下程式碼:
         
class Class1
    {
        /// <summary>
        /// 應用程式的主入口點。
        /// </summary>
        [STAThread]
        static void Main(string[] args)
        {
            string a = "1111";       
            TestByValue(a);

            //輸出a=111.
            Console.WriteLine("a={0}",a);

            TestByReference(ref a);

            //按引用傳值時則會改變,輸出a="".
            Console.WriteLine("a={0}",a);
            Console.ReadLine();
        }

        static void TestByValue(string s)
        {
            //設定值
            s = "";
        }

        static void TestByReference(ref string s)
        {
            //設定值
            s = "";
        }
    }
五、關於C#中的堆和棧
    C#中儲存資料的地方有兩種:堆和棧。
    在傳統的C/C++語言中,棧是機器作業系統提供的資料結構,而堆則是C/C++函式提供的。所以機器有專門的暫存器來指向棧所在的地址,有專門的機器指令實現資料的入棧/出棧動作。其執行效率高,但不過也正因為此,棧一般只支援整數、指標、浮點數等系統直接支援的型別。堆是由C/C++語言提供函式庫來維護的,其記憶體是動態分配的。相對於堆來說,棧的分配速度快,不會有記憶體碎片,但支援的資料有限。
    在C#中,值變數由系統分配在棧上。用來分配固定長度的資料(值型別大都有固定長度)。每一個程式都有單獨的堆疊,其他程式不能訪問。在呼叫函式時,呼叫函式的本地變數都被推入程式的棧中。與C/C++類似,堆用來存放可變長度的資料,不過與C/C++不同的是,C#中資料是存放在託管堆中。
    由於值變數在棧中分配,所以把一個值變數賦給另一個值變數,會在棧中複製兩個相同資料的副本;相反,把一個引用變數賦給另一個引用變數時,會在記憶體中建立對同一個位置的引用。
    在棧中分配相對於堆中分配,有以下特點:
    (1)分配速度快;
    (2)用完以後自動解除分配;
    (3)可以用等號的方式把一個值型別的變數賦給另一個值型別。