值類型,引用類型,棧,堆,ref,out

来源:http://www.cnblogs.com/bb-love-dd/archive/2016/10/30/6012588.html
-Advertisement-
Play Games

在網上收集。。。 C#的值類型,引用類型,棧,堆,ref,out C# 的類型系統可分為兩種類型,一是值類型,一是引用類型,這個每個C#程式員都瞭解。還有托管堆,棧,ref,out等等概念也是每個C#程式員都會接觸到的概念,也是C#程式員面試經常考到的知識,隨便搜搜也有無數的文章講解相關的概念,貌似 ...


在網上收集。。。

 

C#的值類型,引用類型,棧,堆,refout 

C# 的類型系統可分為兩種類型,一是值類型,一是引用類型,這個每個C#程式員都瞭解。還有托管堆,棧,refout等等概念也是每個C#程式員都會接觸到的概念,也是C#程式員面試經常考到的知識,隨便搜搜也有無數的文章講解相關的概念,貌似沒寫一篇值類型,引用類型相關博客的不是好的C#程式員。我也湊個熱鬧,試圖徹底講明白相關的概念。

程式執行的原理

要徹底搞明白那一堆概念及其它們之間的關係似乎並不是一件容易的事,這是因為大部分C#程式員並不瞭解托管堆(簡稱)和線程棧(簡稱),或者知道它們,但瞭解得並不深入,只知道:引用類型保存在托管堆里,而值類型通常保存在棧里。要搞明白那一堆概念的關係,我認為先要明白程式執行的基本原理,從而理解棧和托管堆的作用,才能理清它們的關係。考慮下麵代碼,Main調用Method1Method1調用Method2

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

class Program

{

    static void Main(string[] args)

    {

        var num = 120;

        Method1(num);

    }

 

    static void Method1(int num)

    {

        var num2 = num + 250;

        Method2(num2);

        Console.WriteLine(num);

    }

 

    static void Method2(int i)

    {

        Console.WriteLine(i);

    }

}

大家都知道Windows程式通常是多個線程的,這裡不考慮多線程的問題。程式由Main方法進入開始執行,這時這個(主)線程會分配得到一個1M大小的只屬於它自己的線程棧。1M的的棧空間用於向方法傳遞參數,定義局部變數。所以在Main方法進入Method1前,大家心理面要有一個記憶體圖num壓入線程棧,如下圖:

 

接著把num作為參數傳入Method1方法,同樣在Method1內定義一個局部變數num2,調用加方法得到最後的值,所以在進入Method2前,記憶體圖如下,num是參數,num2是局部變數

 

接著調用Method2的過程雷同,然後退出Method2方法,回到上圖的樣子,再退出Method1方法,再回到第一副圖的樣子,然後退出程式,整個過程如下圖:

 

所以去除那些iffor,多線程等等概念,只保留對象記憶體分配相關概念的話,程式的執行可以簡單總結為如下:

程式由Main方法進入執行,並不斷重覆著“定義局部變數,調用方法(可能會傳參),從方法返回”,最後從Main方法退出。在程式執行過程中,不斷壓入參數和局部變數到線程棧里,也不斷的出棧。

註意,其實壓入棧的還有方法的返回地址等,這裡忽略了。

引用類型和堆

上面的例子我只用了一種簡單的int值類型,目的是為了只關註線程棧的壓棧(生長)和出棧(消亡)。很明顯C#還有種引用類型,引入引用類型,再考慮上面的問題,看下麵代碼:

?

1

2

3

4

5

6

7

8

9

10

11

12

static void Main(string[] args)

{

    var user = new User { Age = 15 };

    var num = 23;

    Console.WriteLine(user.Age);

    Console.WriteLine(num);

}

 

class User

{

    public int Age;

}

我想很多人都應該知道,這時應該引入托管堆的概念了,但這裡我想跟上面一樣,先從棧的角度去考慮問題,所以在調用WriteLine前,記憶體圖應該是這樣的(地址是亂寫的):

 

這也就是人們常說的:對於引用類型,棧里保存的是指向在堆里的實例對象的地址(指針,引用)。既然只是個地址,那麼要獲取一個對象的實例應該有一個根據地址或尋找對象的步驟,而事實正是這樣,如果Console.WriteLine(num),這樣獲取棧里的num的值給WriteLine方法算一步的話,要獲取上面user的實例對象,在運行時是要分兩步的,也就是多了根據地址去尋找托管堆里實例對象的欄位或方法的步驟。IL反編譯上面的Main方法,刪去一些無關代碼後:

?

1

2

3

4

5

//load local 0=>獲取局部變數0(是一個地址)

IL_0012:  ldloc.0

// load field => 將指定對象中欄位的值推送到堆棧上。

IL_0013:  ldfld      int32 CILDemo.Program/User::Age

IL_0018:  call       void [mscorlib]System.Console::WriteLine(int32)

?

1

2

3

//load local 1=>獲取局部變數1(是一個值)

IL_001e:  ldloc.1

IL_001f:  call       void [mscorlib]System.Console::WriteLine(int32)

第二個WriteLine方法前,只需要一個ldloc.1load local 1)讀取局部變數1指令即可獲取值給WriteLine,而第一個WriteLine前需要兩條指令完成這個任務,就是上面說的分兩步。

當然,大家都知道對我們來說,這是透明的,所以很多人喜歡畫這樣的圖去幫助理解,畢竟,我們是感覺不到那個0x0612ecb4地址存在的。

 

也有一種說法就是,引用類型分兩段存儲,一是在托管堆里的值(實例對象),二是持有它的引用的變數。對於局部變數(參數)來說,這個引用就在棧里,而作為類型的欄位變數的話,引用會跟隨這個對象。

欄位和局部變數(參數)

上面圖的托管堆,大家應該看到,作為值類型的Age的值是保存在托管堆里的,並不是保存在棧里,這也是很多C#新手所犯的錯誤:值類型的值都是保存在棧里。

很明顯他們不知道這個結論是在我們上面討論程式運行原理時,局部變數(參數)壓棧和出棧時這個特定的場景下的結論。我們要搞清楚,就像上面代碼一樣,除了可以定義int類型的num這個局部變數存儲23這個值外,我們還可以在一個類型里定義一個int類型Age欄位成員來存儲一個整形數字,這時這個Age很明顯不是儲存在棧,所以結論應該是:值類型的值是在它聲明的位置存儲的。即局部變數(參數)的值會在棧里,作為類型成員的話,會跟隨對象。

當然,引用類型的值(實例對象)總是在托管堆里,這個結論是正確的。

ref和out

C#有值類型和引用類型的區別,再有傳參時有refout這兩個關鍵字使得人們對相關概念的理解更加模糊。要理解這個問題,還是要從棧的角度去理解。我們分四種情況討論:正常傳遞值類型,正常傳遞引用類型,refout)傳遞值類型,refout)傳遞引用類型。

註意,對於運行時來說,refout是一樣,它們的區別是C#編譯器對它們的區別,ref要求初始化好,out沒有要求。因為out沒有要求初始化,所以被調用的方法不能讀取out參數,且方法返回前必須賦值。

正常傳遞值類型

?

1

2

3

4

5

6

7

8

9

10

11

12

static void Main(string[] args)

{

    var num = 120;

    Method1(num);

    Console.WriteLine(num);//輸出=>120

}

 

static void Method1(int num)

{

    Console.WriteLine(num);

    num = 180;

}

這種場景大家都熟悉,Method1的那句賦值是不起作用的,如果要畫圖的話,也跟上面第二幅圖類似:

 

也就是說傳參是把棧里的值複製到Method1num參數Method1操作的是自己的參數,對Main的局部變數完全沒有影響,即影響不到屬於Main方法的棧里的數據。

正常傳遞引用類型

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

static void Main(string[] args)

{

    var user = new User();

    user.Age = 15;

    Method2(user);

    Debug.Assert(user != null);

    Console.WriteLine(user.Age);//輸出=> 18

}

 

static void Method2(User user)

{

    user.Age = 18;

    user = null;

}

留意這裡的Method2的代碼,把Age設為18,影響到了Main方法的user,而把user設為null卻沒有影響。要分析這個問題,還是要先從棧的角度去看,棧圖如下(地址亂寫):

 

看到第二幅圖,大家應該大概明白了這個事實:無論值類型也好,引用類型也好,正常傳參都是把棧里的值複製給參數,從棧的角度看的話,C#預設是按值傳參的。

既然都是按值傳參,那麼引用類型為什麼表現出可以影響到調用方法的局部變數這個跟值類型不同的表現呢?仔細想想也不難發現,這個不同的表現不是由傳參方式不同引起的,而是值類型和引用類型的局部變數(參數)在記憶體的存儲不同引起的。對於Main方法的局部變數userMethod2的參數user在棧里是各自儲存的,棧里的數據(地址,指針,引用)互不影響,但它們都指向同一個在托管堆里的實例對象,而user.Age = 18這一句操作的正是對托管堆里的實例對象的操作,而不是棧里的數據(地址,指針,引用)。num = 180操作的是棧里的數據,而user.Age = 18卻是托管堆,就是這樣造成了不同的表現。

對於user = null這一句不會響應Main的局部變數,看了第三幅圖應該也很容易明白,user = nulluser.Age = 18不一樣,user = null是把棧里的數據(地址,指針,引用)設空,所以並不會影響Mainuser

這裡再補充一下,對引用類型來說,var user = nullvar user = new User()user1 = user2都會影響棧里的數據(地址,指針,引用),第一個會設null,第二個會得到一個新的數據(地址,指針,引用),第三個跟上面傳參一樣,都是棧數據複製。

ref(out)傳遞值類型

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

static void Main(string[] args)

{

    var num = 10;

    Method1(num);

    Console.WriteLine(num);//輸出=> 10

    Method3(ref num);

    Console.WriteLine(num);//輸出=> 28

}

 

static void Method1(int num)

{

    Console.WriteLine(num);

    num = 18;

}

 

static void Method3(ref int num)

{

    Console.WriteLine(num);

    num = 28;

}

代碼很簡單,而且輸出應該都很清楚,沒有難度。ref的使用看似簡單平常,背後其實是C#為我們做了大部分工作。畫圖的話,棧圖如下(地址亂寫):

 

看到這圖,不少人應該迷惑了,Method3的參數明明寫的是int類型的num,怎麼在棧里卻是一個指針(地址,引用)呢?這其實C#“欺騙了我們,IL反編譯看看:

 

可以看到,加了refout)的Method3編譯出來的方法參數是不一樣,再來看看方法里對參數取值的IL代碼:

?

1

2

3

4

5

6

7

8

9

//這是Method1的代碼

//load arg 0=>讀取索引0的參數,直接就是一個值

IL_0001:  ldarg.0

 

//這是Method3的代碼

//load arg 0=>讀取索引0的參數,這是一個地址

IL_0001:  ldarg.0

//將位於上面地址處的 int32 值作為 int32 載入到堆棧上。

IL_0002:  ldind.i4

可以看到,同樣是獲取參數值給WriteLineMethod1只需一個指令,而Method3則需要2個,即多了一個根據地址去尋值的步驟。不難想到,賦值也有同樣的區別:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

//Method1

//把18放入棧中

IL_0008:  ldc.i4.s   18

//store arg=> 把值賦給參數變數num

IL_000a:  starg.s    num

 

//Method3

//load arg 0=>讀取索引0的參數,這是一個地址

IL_0009:  ldarg.0

//把28放入棧中

IL_000a:  ldc.i4.s   28

//在給定的地址存儲 int32 值。

IL_000c:  stind.i4

沒錯,雖然同樣是num = 5這樣一個對參數的賦值語句,有沒有refout)關鍵字,實際上運行時發生的事情是不一樣的。有refout)的方法跟上面取值一樣有給定地址然後去操作(這裡是賦值)的指令。

看到這裡大家應該明白,給參數加了refout)後,參數才是引用傳遞,這時傳遞的是棧地址(指針,引用),否則就是正常的值傳遞--棧數據複製。

ref(out)傳遞引用類型

加了refout)的引用類型的參數有什麼奧秘,這個留給大家去思考。可以肯定的是,還是從棧的角度去考慮的話,跟值類型是沒有區別的,都是傳遞棧地址。

我個人認為,貌似給引用類型加refout)沒什麼用處。

總結

在考慮這一大堆概念問題時,我們首先要搞明白程式執行的基本原理,只不過是棧的生長和消亡的過程。明白這個過程後,要學會從棧的角度去思考問題,那麼很多事情將會迎刃而解。為什麼叫類型和引用類型呢?其實這個引用是從棧的角度去考慮的,在棧里,值類型的數據就是值,引用類型在棧里只是一個地址(指針,引用)。還要註意到,變數除了可以是一個局部變數(參數)外,還可以作為一個類型的欄位成員存在。知道這些後,值類型的對象是存儲在那裡?這些問題應該就一清二楚了。最後就是明白C#預設是按值傳參的,也就是把棧里的數據賦值給參數,這跟在同一個方法內把一個變數賦值給同一類型的另一個變數是一樣的,而加了refout)為什麼這個神奇,其實是C#背後做了更多的事情,編譯成不同的IL代碼了。

 


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

-Advertisement-
Play Games
更多相關文章
  • 經過一個多月晚上的時間,終於把開源物聯網通訊框架ServerSuperIO成功移植到Windows10 IOT上,暫時全名:ServerSuperIO.WinIOT(以後可能還會移植到Ubuntu上)。以後可以把ServerSuperIO框架應用到嵌入式設備上,移植的成功具有標誌性意義,意味著軟體和... ...
  • 文檔目錄 本節內容: 簡介 Asp.net Core 安裝 安裝Nuget包 配置 測試 Asp.net 5.x 安裝 安裝Nuget包 配置 測試 安裝 安裝Nuget包 配置 測試 安裝 安裝Nuget包 配置 測試 簡介 來自它的網頁:“...使用一個Swagger-enabled Api,你 ...
  • 前言: 由於先前用python+opencv做了一會兒人臉識別。(其實是別人做的,我是打醬油的)。 用winform做了當時用的一個功能界面。打開攝像頭併進行拍照保存。 界面預覽: 此次,利用的是winform+AForge框架。AForge是全過程都在用的,必須要有。 介紹一下製作過程: 1.創建 ...
  • 一、簡介 首先來看看.net的發展中的各個階段的特性:NET 與C# 的每個版本發佈都是有一個“主題”。即:C#1.0托管代碼→C#2.0泛型→C#3.0LINQ→C#4.0動態語言→C#4.5非同步編程 所謂的非同步編程是利用CPU空閑時間和多核的特性,它所返回的Task或Task是對await的一個 ...
  • 文檔目錄 本節內容: 簡介 安裝 安裝Nuget包 設置模塊依賴 配置你的實體 創建控制器 示例 獲取實體列表 請求 響應 獲取單個實體 請求 響應 獲取單個實體及導航屬性 請求 響應 查詢 請求 響應 創建一個新實體 請求 響應 獲取元數據 請求 響應 示例項目 安裝Nuget包 設置模塊依賴 配 ...
  • Quartz.NET是一個非常強大的作業調度框架,適用於各種定時執行的業務處理等,類似於WINDOWS自帶的任務計劃程式,其中運用Cron表達式來實現各種定時觸發條件是我認為最為驚喜的地方。 Quartz.NET主要用到下麵幾個類: IScheduler --調度器 IJobDetail --作業任 ...
  • using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ref_out { class Program { static void Method1(ref int ...
  • 文檔目錄 本節內容: 創建動態Web Api控制器 ForAll 方法 重寫 ForAll ForMethods Http 動詞 WithVerb 方法 HTTP 特性 命名約定 Api 瀏覽器 RemoteService 特性 動態Javascript代理 AJAX 參數 單獨服務腳本 Angul ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...