漫談值類型和引用類型

来源:https://www.cnblogs.com/jingzhe2004/archive/2019/11/10/11828766.html
-Advertisement-
Play Games

一.前言 從這個簡單程式的輸出結果,你想到了什麼?是不是與你心中想的結果不一致?是不是覺得輸出的結果應該為:i is 1,o is 8,o2 is 8 二.程式執行前 圖 2 我們都知道,每一個方法在執行前,操作系統會給方法內每個變數分配記憶體空間。從圖2中就可以看出,在執行前各變數(i,o,o2)已 ...


 

一.前言

                            圖1

        從這個簡單程式的輸出結果,你想到了什麼?是不是與你心中想的結果不一致?是不是覺得輸出的結果應該為:i is 1,o is 8,o2 is 8

二.程式執行前

                            圖2

                                                                                圖 2

       我們都知道,每一個方法在執行前,操作系統會給方法內每個變數分配記憶體空間。從圖2中就可以看出,在執行前各變數(i,o,o2)已分配了記憶體,且各自都有初始值。

       從圖中,可以發現變數i和變數o,o2有些許不同。變數i在記憶體中存儲的值和程式中的值是一樣的,都是0;變數o,o2在記憶體中存儲的值和程式中的值不一樣,記憶體中存儲的值是一個地址(0x00000000),程式中的值是null,那變數o,o2的null值存儲在哪呢?為什麼變數i和變數o,o2會有如此大的不同呢?

       我們都知道,C#有兩大類型:值類類型和引用類型。圖2中int屬於值類型,object屬於引用類型。接下來,介紹一下值類型和引用類型:

      1.值類型的值存儲在記憶體棧上,引用類型的值存儲在記憶體堆中。

       園中有很多博文這麼描述,我用程式驗證了一下全局的值類型變數的值,靜態的值類型變數的值,引用類型實例中值類型成員的值,如下圖3

                        圖3

                                                                                                                     圖 3

       從圖中,可以看出變數(j,o,seg,st)的值應該是在同一個存儲區域中,而變數(gi)是在另外一個存儲區域中。引用類型Student的成員Age的地址還未分配。所以說值類型的值存儲在記憶體棧上是不准確的。

       查找了一些資料,記憶體格局分為四個區:

      1)全局數據區:存放全局變數,靜態變數,常量的值

      2)代碼區:存放程式代碼

      3)棧區:存放為運行而分配的局部變數,參數等

      4)堆區:自由存儲區。

      更為準確的說,方法體內的值類型變數的值存儲在記憶體棧上,引用類型變數的值存儲在記憶體堆上。由於對象實例是引用類型變數的值,而對象實例成員只是對象實例的一部分,所以其隨對象實例整個存儲在記憶體堆上。

       或許眼尖的園友發現了,上面那句話還是不對,結構體StructEg的引用類型成員Name的數據就沒有存儲在記憶體棧上。從圖3看,結構體變數seg的數據分成兩部分,值類型成員數據存儲在記憶體棧上,引用類型成員數據存儲在記憶體堆上。

       所以確切的說:方法體內的預定義的值類型(int,bool,char)變數的數據存儲在記憶體棧上,引用類型變數的值存儲在托管堆中,結構體的值類型成員的值存儲在記憶體棧上,結構體的引用類型成員的值存儲在記憶體堆中。(下麵介紹的值類型基本是預定義的值類型和只包含值類型成員的結構體,一般包含引用類型成員的都定義成類)

       2. 值類型變數直接存儲數據,而引用類型變數則存儲對數據的引用

       這句話怎麼理解呢?這句話中關鍵詞是”存儲”,其實還是在描述程式中的變數在記憶體棧中的表現。

       值類型變數在記憶體棧中存儲的是其在程式中的變數值,引用類型變數在記憶體棧中存儲的是其程式中的值在記憶體堆中的引用。(當然值類型變數和引用類型變數都是方法體內的局部變數或參數)

       3.引用類型變數賦值過程

       1)分配記憶體堆空間:我們都知道要存儲數據,首先得申請記憶體空間。引用類型變數在new實例化時,系統在記憶體堆中分配空間。

       2)更新地址:把引用類型變數在記憶體棧中存儲的值更新成新的值(新值為新分配的記憶體堆的首地址)。至此,引用類型變數指向了新的記憶體空間。

       3)填充值:把初始化值填充到記憶體堆中。

       可能有些園友會說,瞭解這個有什麼意義呢?那我就簡單的說一個現象:

       1)在學習方法理論時,傳參會有這樣的描述:值類型按值傳遞,傳遞的是對象的副本,對已調用方法中的對象的更改對原始對象無影響引用類型的對象按值傳遞傳遞的是對對象的引用,使用此引用更改對象的成員,此更改將影響原始對象。

       其實,這裡究其原理,就是因為值類型與引用類型的值的不同存儲位置,來描述其傳參後的影響。

       所以,有時我們為了影響值類型實參的值,而在形參前面加ref或out;有時我們為了不影響引用類型實參的值,而採用深拷貝的方式傳遞參數值。

       理論是為了指導實踐,當瞭解了其原理,在實踐時,我們才會顯得踏實。

三.執行變數i賦值

                             圖4

        從圖中可以看出,執行後,值類型變數在記憶體中存儲的值和其在程式中的值是一樣的,都是1。

四.執行object o=i;

                              圖5

                                                                                           圖 5

       從圖中可以看出,引用類型變數o的值變成1了,在記憶體棧中存儲的值更新成新地址了。通過前面分析,我們知道變數o指向了1的新地址。值類型變數的值存儲在記憶體棧中,引用類型變數的值存儲在記憶體堆中,記憶體棧中的值是如何到記憶體堆中的?這就是本節要介紹的第二個重要概念,裝箱和拆箱。

       4.1.裝箱

        1.裝箱:裝箱是把值類型到object類型或值類型到其實現的介面的隱式轉換。

        園中很多博文介紹:值類型轉換為引用類型,就叫裝箱。我覺得這表述不太準確,如下圖6

                 圖6

                                                                            圖 6

           從圖6中可以看出,值類型不能隨意的轉換為引用類型,它只能隱式轉換為以下兩種引用類型:

           1)object類型;

           2)值類型實現的介面

           2.裝箱的過程

           前面已介紹了引用類型變數賦值過程了,裝箱步驟也類似:

           1)分配新的記憶體空間

           2)更改地址

           3)填充值:從值類型變數處拷貝一份值,存儲到新分配的記憶體堆中。

           4.2.拆箱

           1.拆箱:從 object 類型到值類型或從介面類型到實現該介面的值類型的顯式轉換。

           2.拆箱過程

           1)檢查對象實例,以確保它是給定值類型的裝箱值。若不能顯式轉換,則拋異常。

           意思是:裝箱時的值類型和拆箱時的值類型要完全一致(哪怕類型相容也不行,如下圖7中的裝箱前的類型是short,拆箱後的類型是int,就會產生異常)。如圖7

                       圖7

                                                                               圖 7

         2)驗證成功後,複製實例的值到值類型變數中

        相對於簡單的賦值而言,裝箱和拆箱過程需要進行大量的計算,所以其對性能會有較大的損耗。特別裝箱時,要創建新的對象實例,要在記憶體堆上分配新的記憶體空間,在分配新的記憶體空間時,可能會引起垃圾回收(垃圾回收對性能損耗非常大,具體垃圾回收為什麼會有很大的性能損耗,網上相關介紹很多,在此不做介紹)。

        或許有園友會說,平時裝箱/拆箱操作不多,其實在你不經意間,存在很多裝箱操作

        1)string s=string.Format(“{0}”,i);//i為值類型數據---典型的字元串格式化

四.執行object o2=o;

                          圖8

                                                                                          圖 8

                           圖9

                                                                                                  圖 9

        無論是值類型變數賦值還是引用類型變數賦值,都是把數據複製一份,然後賦給另一個變數。只是引用類型變數在記憶體棧中存儲的是其值在記憶體堆中的地址。所以引用類型變數間賦值,就使兩個變數指向了同一個記憶體堆空間。如上圖8,圖9

五.執行o=8

       此時,或許有人會說,這句不是表示對引用類型變數進行操作嗎?賦值了8後,它在記憶體堆內的值應該是8了,由於o2與o都指向記憶體堆內的同一個地址,所以o2的值也應該也是8。

       呵呵,請註意,8是值類型,o是引用類型,類型不一樣,要進行裝箱操作,裝箱的過程中會創建新的實例分配新的記憶體空間。所以引用類型變數o指向了新的記憶體堆空間了,由於引用類型變數o2沒有做任何操作,所以此時引用類型變數o和o2在記憶體棧中存儲的地址不一樣了,指向的記憶體堆地址也不一樣了,所以它們的值也就不一樣了。如下圖10,圖11

                                  圖10

                                                                                                       圖 10

                                 圖11

                                                                                                     圖 11

        那如何讓o的值改變,o2的值也同時變化,就要改變o對應的記憶體堆內的值。

六.最後執行Console.WriteLine("i is " + i.ToString() + ",o is " + o.ToString() + ",o2 is " + o2.ToString());

        所以最後結果的值是:i is 1,o is 8,o2 is 1

七.總結:

      通篇通過一則簡短的賦值程式,介紹了

      1)C#兩大類型:值類型與引用類型

      2)值類型與引用類型互相賦值,引出的裝箱、拆箱操作

      其中簡要介紹了裝箱操作會有比較大的性能損耗,特別是垃圾回收。

      最後,通過兩張圖來簡要概括下本篇博文的內容:

      1)C#兩大類型:

                               圖12

         2)變數賦值

                              圖12

 

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • me:5次 標準答案:5 次,因為從 0 開始,到 10 結束,步進為 2。 1. 下麵的迴圈會列印多少次"I Love FishC"? me:錯誤,迴圈應該使用的是一個數組,比如range() 標準答案:會報錯,上節課的課後習題我們提到了 in 是“成員資格運算符”,而不是像 C 語言那樣去使用 ...
  • 概述首先同步下項目概況:上篇文章分享了,路由中間件 - 日誌記錄,這篇文章咱們分享:路由中間件 - 捕獲異常。當系統發生異常時,提示 “系統異常,請聯繫管理員!”,併發送 panic 告警郵件。什麼是異常?在 Go 中異常就是 panic,它是在程式運行的時候拋出的,當 panic 拋出之後,如果在 ...
  • [TOC] 這篇博客及之後的系列,我會向大家介紹各種驗證碼的識別。包括普通圖形驗證碼,極驗滑動驗證碼,點觸驗證碼,微博宮格驗證碼。 一.普通圖形驗證碼 之前的博客已向大家介紹了簡單的圖形驗證碼的處理過程,但是會和實際的有所差別,這是因為驗證碼內的多餘線條與圖案干擾了圖片的識別。因此,對於這種情況,需 ...
  • 前言 Java文件的運行過程: 1,javac.exe:編譯器 2,java.exe:解釋器 微軟shell下運行實例: C:\Users\Administrator>cd D:\文檔\JAVALIAnxi\ C:\Users\Administrator>d: D:\文檔\JAVALIAnxi>ja ...
  • 把所有的節點用一根直線串起來 連續存儲[數組] 什麼叫做數組:元素類型相同,大小相等 重點看代碼吧,需要註意的都在註釋里,多敲幾遍,當然了,有些功能還沒有實現,以後再實現 ...
  • 概述首先同步下項目概況:上篇文章分享了,路由中間件 - Jaeger 鏈路追蹤(理論篇)。這篇文章咱們分享:路由中間件 - Jaeger 鏈路追蹤(實戰篇)。說實話,這篇文章確實讓大家久等了,主要是裡面有一些技術點都是剛剛研究的,沒有存貨。先看下咱們要實現的東西:API 調用了 5 個服務,其中 4 ...
  • 一、Dubbo誕生背景(摘自Dubbo官網-入門-背景) 二、Dubbo架構圖(摘自Dubbo官網-入門-架構) 三、Dubbo核心依賴(jar包):dubbo、zkclient 四、Dubbo項目搭建的方式:配置文件式、註解式 五、Dubbo項目配置文件的核心配置: (一)配置文件式 1. 服務提 ...
  • 1. 模塊 1.1 什麼是模塊 別人寫好的函數、變數、方法放在一個文件里 (這個文件可以被我們直接使用)這個文件就是個模塊 常見的場景:一個模塊就是一個包含了python定義和聲明的文件,文件名就是模塊名字加上.py的尾碼。 但其實import載入的模塊分為四個通用類別: 1.使用python編寫的 ...
一周排行
    -Advertisement-
    Play Games
  • 概述:在C#中,++i和i++都是自增運算符,其中++i先增加值再返回,而i++先返回值再增加。應用場景根據需求選擇,首碼適合先增後用,尾碼適合先用後增。詳細示例提供清晰的代碼演示這兩者的操作時機和實際應用。 在C#中,++i 和 i++ 都是自增運算符,但它們在操作上有細微的差異,主要體現在操作的 ...
  • 上次發佈了:Taurus.MVC 性能壓力測試(ap 壓測 和 linux 下wrk 壓測):.NET Core 版本,今天計劃準備壓測一下 .NET 版本,來測試並記錄一下 Taurus.MVC 框架在 .NET 版本的性能,以便後續持續優化改進。 為了方便對比,本文章的電腦環境和測試思路,儘量和... ...
  • .NET WebAPI作為一種構建RESTful服務的強大工具,為開發者提供了便捷的方式來定義、處理HTTP請求並返迴響應。在設計API介面時,正確地接收和解析客戶端發送的數據至關重要。.NET WebAPI提供了一系列特性,如[FromRoute]、[FromQuery]和[FromBody],用 ...
  • 原因:我之所以想做這個項目,是因為在之前查找關於C#/WPF相關資料時,我發現講解圖像濾鏡的資源非常稀缺。此外,我註意到許多現有的開源庫主要基於CPU進行圖像渲染。這種方式在處理大量圖像時,會導致CPU的渲染負擔過重。因此,我將在下文中介紹如何通過GPU渲染來有效實現圖像的各種濾鏡效果。 生成的效果 ...
  • 引言 上一章我們介紹了在xUnit單元測試中用xUnit.DependencyInject來使用依賴註入,上一章我們的Sample.Repository倉儲層有一個批量註入的介面沒有做單元測試,今天用這個示例來演示一下如何用Bogus創建模擬數據 ,和 EFCore 的種子數據生成 Bogus 的優 ...
  • 一、前言 在自己的項目中,涉及到實時心率曲線的繪製,項目上的曲線繪製,一般很難找到能直接用的第三方庫,而且有些還是定製化的功能,所以還是自己繪製比較方便。很多人一聽到自己畫就害怕,感覺很難,今天就分享一個完整的實時心率數據繪製心率曲線圖的例子;之前的博客也分享給DrawingVisual繪製曲線的方 ...
  • 如果你在自定義的 Main 方法中直接使用 App 類並啟動應用程式,但發現 App.xaml 中定義的資源沒有被正確載入,那麼問題可能在於如何正確配置 App.xaml 與你的 App 類的交互。 確保 App.xaml 文件中的 x:Class 屬性正確指向你的 App 類。這樣,當你創建 Ap ...
  • 一:背景 1. 講故事 上個月有個朋友在微信上找到我,說他們的軟體在客戶那邊隔幾天就要崩潰一次,一直都沒有找到原因,讓我幫忙看下怎麼回事,確實工控類的軟體環境複雜難搞,朋友手上有一個崩潰的dump,剛好丟給我來分析一下。 二:WinDbg分析 1. 程式為什麼會崩潰 windbg 有一個厲害之處在於 ...
  • 前言 .NET生態中有許多依賴註入容器。在大多數情況下,微軟提供的內置容器在易用性和性能方面都非常優秀。外加ASP.NET Core預設使用內置容器,使用很方便。 但是筆者在使用中一直有一個頭疼的問題:服務工廠無法提供請求的服務類型相關的信息。這在一般情況下並沒有影響,但是內置容器支持註冊開放泛型服 ...
  • 一、前言 在項目開發過程中,DataGrid是經常使用到的一個數據展示控制項,而通常表格的最後一列是作為操作列存在,比如會有編輯、刪除等功能按鈕。但WPF的原始DataGrid中,預設只支持固定左側列,這跟大家習慣性操作列放最後不符,今天就來介紹一種簡單的方式實現固定右側列。(這裡的實現方式參考的大佬 ...