值類型和引用類型在棧和堆中的分配

 

    類型基礎及背後的工做原理   數據在內存中的分配與傳遞    值類型和引用類型它們在內存分配與傳遞上的區別程序員

內存分配編程

    首先要了解一下內存中棧和堆的概念。spa

    棧(Stack)線程

     ##棧是一種先進後出的內存結構。3d

     方法的調用追蹤就是在棧上完成的。好比咱們有一個main方法(程序入口), 在main方法中會調用一個GetPoint的方法。在線程執行時,會將main方法壓入棧底(包括編譯好的方法指令,參數,和方法內部變量),而後再將GetPoint的方法壓入棧底,GetPoint中沒有調用其它方法,壓棧完畢。出棧順序是先進後出,也就是後進先出,棧頂的方法GetPoint先執行完畢,而後出棧,所佔內存清空,接着main方法執行後出棧,所佔內存清空。指針

//示意圖:本身腦補吧...

  從上面方法的壓棧出棧中能夠看出:code

     ##棧只能在一端對數據進行操做,也就是棧頂端進行操做。’orm

     ##棧也是一種內存自我管理的結構,壓棧自動分配內存,出棧自動清空所佔內存。對象

  另外值得注意的兩點:blog

     ##棧中的內存不能動態請求,只能爲大小肯定的數據分配內存,靈活性不高,可是棧的執行效率很高。

     ##棧的可用空間並不大,因此咱們在操做分配到棧上的數據時要注意數據的大小帶來的影響。

   堆(Heap)

     ##堆與棧有所區別,堆在C#中用於存儲實實例對象,能存儲大量數據,並且堆可以動態分配存儲空間。

     ##相比棧只能在一端操做,堆中的數據能夠隨意存取。

     ##但堆的結構使得堆的執行效率不如棧高,並且不能自動回收使用過的對象。對於堆中的內存回收,C++程序員須要進行手動回收,這也是C++編程值得注意的一點,不然很容易形成內存溢出。而對於.NET程序員,平臺提供了垃圾回收(GC)機制,能夠自動回收堆中過時的對象(實現原理大概就是當發現沒有「引用」指向此對象時,代表此對象能夠回收,此文主要討論值類型和引用類型,對於GC,感興趣的能夠搜索相關資料)。

    值類型和引用類型在棧和堆中的分配

    這兒有兩個原則:

    1.建立引用類型時,runtime會爲其分配兩個空間,一塊空間分配在堆上,存儲引用類型自己的數據,另外一個塊空間分配在棧上,存儲對堆上數據的引用,實際上存儲的堆上的內存地址,也就是指針。

    2.建立值類型時, runtime會爲其分配一個空間,這個空間分配在變量建立的地方,如:

       ##若是值類型是在方法內部建立,則跟隨方法入棧,分配到棧上存儲。

      ##若是值類型是引用類型的成員變量,則跟隨引用類型,存儲在堆上。(對象的成員變量)

   在此咱們舉例說明。

 定義一個Point類:  

 public class Point
   {
        public double PointX { get; set; }
        public double PointY { get; set; }   
    }

StartProgram類,有方法Start()和InitialPoint():

複製代碼
  class StartProgram
    {
        void Start()
        {
            double pointX = 100.1;
            InitialPoint(pointX);
        }
        void InitialPoint(double pointX)
        {
            var point = new Point();
            point.PointX = pointX;
        }
    }
複製代碼

示例分析:假設主線程從Start()進入執行,咱們從分析一下方法中的變量在內存中的大體分配狀況,不深究細節。

 首先將Start()方法指令壓入棧底,而後壓入局部變量pointX;緊接着將InitialPoint()方法壓入棧底,形參pointX壓入棧底,在堆上實例化Point對象(包括其成員變量PointX和PointY),並在棧上建立point變量指向堆上的Point對象,最後給成員變量PointX賦值,參考圖以下:

    注:注意不要混淆code中的pointx,雖然變量名相同,可是它們是不一樣的變量。 

      

 

數據傳遞

   按值傳遞原則

   在C#中數據傳遞默認按值傳遞,先看一個示例。

 如今有一個結構體PointSturct, 一個類PointClass:

  public struct PointStruct
    {
        public double PointX { get; set; }
        public double PointY { get; set; }   
    }
  public class PointClass
    {
        public double PointX { get; set; }
        public double PointY { get; set; }   
    }

並在一個方法中執行執行如下代碼:

複製代碼
1  void Excute()
2  {
3       var pointStruct1 = new PointStruct();
4       var pointClass1 = new PointClass();
5       var pointStruct2 = pointStruct1;
6       var pointClass2 = pointClass1;
7   }
複製代碼

示例分析:第3,4行代碼分別建立了一個結構體pointStruct1和一個類實例pointClass1, 結合上面的內存分配規則,對於pointSturct1,會在棧上分配內存存儲其數據自己,對於pointClass1,會在堆上分配內存存儲實例,且在棧上存儲指向堆上實例的指針,參考圖以下:

     通過執行5,6行代碼後,內存分配應該是怎樣的呢? 對於值類型(pointStruct1),會在棧上開闢一塊新的空間,將數據徹底複製過去,所以pointStruct2和pointStruct1是互相獨立的,對其中一個的修改不會影響到另外一個;對於引用類型(pointClass1),也會在棧上開闢一個新的空間,將棧上的數據(指向堆上實例的指針)複製到新的空間, 可是注意,此處複製的是指針,也就是說棧上的兩個變量pointClass1和pointClass2雖然是不一樣的空間,可是它們的存儲內容---指針(內存地址), 都是指向堆上的同一實例,因此當經過pointClass2對實例的數據進行修改之後,經過pointClass1再訪問實例的數據,將會是修改過的數據,反之亦然,參考圖以下:

    參數傳遞

     當程序中進行參數傳遞的時候,也是默認按值傳遞,值類型複製數據自己,造成獨立的數據塊,引用類型複製引用,指向同一實例。簡單一點就是傳遞時複製棧上的數據到新的棧上空間。

咱們將以前的StartProgram類中的方法改爲以下 :

複製代碼
class StartProgram
{
   void Start()
   {
      double pointX1 = 100.1;
      var point1 = new Point();
      point1.PointX = 200.1;
      InitialPoint(pointX1, point1);//值類型複製數據自己 對象傳遞 複製引用,指向同一實例
      Console.WriteLine(string.Format("pointX1:{0}", pointX1));
      Console.WriteLine(string.Format("point1.PointX:{0}", point1.PointX));
      Console.ReadKey();
    }
    void InitialPoint(double pointX2, Point point2)
    {
       pointX2 = 300.1;
       point2.PointX = pointX2;
    }
 }
/*Output:pointX1:100.1
         point1.PointX:300.1 
*/
複製代碼

示例分析:從輸出結果能夠看到,pointX1仍是原來的值,沒有受到pointX2影響,而point1.PointX的值是point2對PointX更改後的值。在內存中,將值類型pointX1傳遞給pointX2後,在棧上造成兩個獨立的內存塊,所以對pointX2更改後,並不會影響到pointX1;而對於引用類型point1,傳遞給point2後,它們兩塊內存存儲的指針指向同一實例,所以再InitialPoint()方法內對point2.PointX賦值爲300.1後,再Start()方法裏面取point1取PointX的值,也是300.1。

既然point1和point2指向同一實例,那麼若是咱們在InitialPoint()方法的最後將point2設置爲null,會不會影響到Start()方法裏的point1呢?用point.PointX取值的時候,會不會獲得實例爲null的異常呢?

複製代碼
 void InitialPoint(double pointX2, Point point2)
 {
    pointX2 = 300.1;
    point2.PointX = pointX2;
    point2 = null;//

//將point2設置爲null,並非將堆上的實例變爲null,而是設置棧上的point2這塊存儲指針的內存爲null
//棧上point1和point2雖然指向同一實例,可是它們是兩塊不一樣的內存,因此將point2設置爲null後,point1仍然指向堆上的實例

 }
 /*Output:pointX1:100.1
          point1.PointX:300.1 
 */
複製代碼

示例分析:仍是會獲得以前的結果,沒有檢測到null異常。這不難想象,由於咱們將point2設置爲null,並非將堆上的實例變爲null,而是設置棧上的point2這塊存儲指針的內存爲null,而棧上point1和point2雖然指向同一實例,可是它們是兩塊不一樣的內存,因此將point2設置爲null後,point1仍然指向堆上的實例,而且point2設置爲null是在對堆上的實例進行更新之後,所以point1.PointX的到的值是更新後的值,參考圖以下:

    按引用傳遞(Ref和Out關鍵字)

    注:Ref和Out的區別在於Ref在傳遞前須要初始化。

   咱們知道C#中的Ref和Out關鍵字能夠在值類型的傳參上實現跟引用類型同樣的效果,那麼在引用類型參數上加入ref和out關鍵字跟默認的引用類型傳參有什麼區別呢?不少人以爲應該沒有什麼用,其實否則,咱們繼續將StartProgram類的方法改成按ref傳遞,看看會有什麼不一樣。

複製代碼
class StartProgram
{void Start()
    {
        double pointX1 = 100.1;
        var point1 = new Point();
        point1.PointX = 200.1;
        InitialPoint(ref pointX1, ref point1);
        Console.WriteLine(string.Format("pointX1:{0}", pointX1));
        if (point1 != null) Console.WriteLine(string.Format("point1.PointX:{0}", point1.PointX));
        else Console.WriteLine(string.Format("point1 is null"));
        Console.ReadKey();
    }
    void InitialPoint(ref double pointX2, ref Point point2)
    {
        pointX2 = 300.1;
        point2.PointX = pointX2;
        point2 = null;
    }
    /*Output:
pointX1:300.1 point1 is null */
}
複製代碼

示例分析:從運行結果能夠看到,對於值類型, pointX2對值的更改影響到了pointX1;對於引用類型,將point2設置爲null後,point1也變成了null,以前咱們沒有加ref參數的時候,point2設置爲null,並不會影響到point1自己。咱們能夠看到,經過加入ref和out參數後,在內存中並非像值傳遞同樣將棧上的數據拷貝一份到新的空間。在這裏,我並無去研究C#對ref和out參數在內存上的實現原理,可是能夠想到,要實現這種效果並不難,在按引用傳遞時咱們將棧上的變量的地址(如存儲pointX1,point1的內存地址)copy到新的棧內存空間中,這樣就能夠將新的變量和舊的變量關聯起來,達到互相影響的效果。

Summary

本文從內存中棧和堆的結構特色出發,分析了C#值類型和引用類型在棧和堆上的分配狀況,接着分析了數據傳遞過程,包括按值傳遞(賦值,參數傳遞),按引用傳遞(ref,out關鍵字),僅供參考。

相關文章
相關標籤/搜索