天天看點

值類型和引用類型的存儲

值類型變量所占用的記憶體空間位于線程堆棧中,而引用類型變量所引用的對象生存于托管堆中。

以下轉載:

  一、值類型和引用類型變量的存儲

    首先,變量是存儲資訊的基本單元,而對于計算機内部來說,變量就相當于一塊記憶體空間。

    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)可以用等号的方式把一個值類型的變量賦給另一個值類型。