亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb

首頁 > 編程 > C# > 正文

一看就懂:圖解C#中的值類型、引用類型、棧、堆、ref、out

2020-01-24 01:48:44
字體:
來源:轉載
供稿:網友

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

程序執行的原理

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

復制代碼 代碼如下:

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的的??臻g用于向方法傳遞參數,定義局部變量。所以在Main方法進入Method1前,大家心理面要有一個”內存圖“:把num壓入線程棧,如下圖:

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

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

所以去除那些if,for,多線程等等概念,只保留對象內存分配相關概念的話,程序的執行可以簡單總結為如下:

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

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

引用類型和堆

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

復制代碼 代碼如下:

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方法,刪去一些無關代碼后:

復制代碼 代碼如下:

//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)

復制代碼 代碼如下:

//load local 1=>獲取局部變量1(是一個值)
IL_001e:  ldloc.1
IL_001f:  call       void [mscorlib]System.Console::WriteLine(int32)

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

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

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

字段和局部變量(參數)

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

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

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

ref和out

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

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

正常傳遞值類型

復制代碼 代碼如下:

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的那句賦值是不起作用的,如果要畫圖的話,也跟上面第二幅圖類似:

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

正常傳遞引用類型

復制代碼 代碼如下:

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方法的局部變量user和Method2的參數user在棧里是各自儲存的,棧里的數據(地址,指針,引用)互不影響,但它們都指向同一個在托管堆里的實例對象,而user.Age = 18這一句操作的正是對托管堆里的實例對象的操作,而不是棧里的數據(地址,指針,引用)。num = 180操作的是棧里的數據,而user.Age = 18卻是托管堆,就是這樣造成了不同的表現。

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

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

ref(out)傳遞值類型

復制代碼 代碼如下:

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反編譯看看:

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

復制代碼 代碼如下:

//這是Method1的代碼
//load arg 0=>讀取索引0的參數,直接就是一個值
IL_0001:  ldarg.0
 
//這是Method3的代碼
//load arg 0=>讀取索引0的參數,這是一個地址
IL_0001:  ldarg.0
//將位于上面地址處的 int32 值作為 int32 加載到堆棧上。
IL_0002:  ldind.i4


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

復制代碼 代碼如下:

//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這樣一個對參數的賦值語句,有沒有ref(out)關鍵字,實際上運行時發生的事情是不一樣的。有ref(out)的方法跟上面取值一樣有給定地址然后去操作(這里是賦值)的指令。

看到這里大家應該明白,給參數加了ref(out)后,參數才是引用傳遞,這時傳遞的是棧地址(指針,引用),否則就是正常的值傳遞--棧數據復制。

ref(out)傳遞引用類型

加了ref(out)的引用類型的參數有什么奧秘,這個留給大家去思考??梢钥隙ǖ氖?,還是從棧的角度去考慮的話,跟值類型是沒有區別的,都是傳遞棧地址。

我個人認為,貌似給引用類型加ref(out)沒什么用處。惡魔

總結

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

發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb
日韩欧美a级成人黄色| 亚洲久久久久久久久久久| 成人激情视频免费在线| 国产成人精品999| 欧美巨大黑人极品精男| 亚洲最大福利视频网| 欧美日韩亚洲国产一区| 亚洲国产高清福利视频| 永久免费精品影视网站| 久久夜色精品国产欧美乱| 亚洲综合av影视| 日韩欧美在线观看视频| 午夜精品www| 亚洲sss综合天堂久久| 国产精品欧美日韩| 亚洲999一在线观看www| 亚洲成人精品久久| 久久人91精品久久久久久不卡| 国产一区二区三区在线观看视频| 亚洲精品国偷自产在线99热| 国产精品美女主播| 欧美成人性生活| 97超级碰碰碰| 亚洲男人的天堂在线| 一本色道久久综合亚洲精品小说| 欧美另类极品videosbest最新版本| 国产美女高潮久久白浆| 97香蕉久久夜色精品国产| 成人在线视频福利| 国a精品视频大全| 亚洲欧洲美洲在线综合| 国产精品444| 亚洲字幕一区二区| 亚洲最大福利视频| 亚洲免费电影一区| 欧美性猛交xxxx免费看久久久| 91天堂在线观看| 97精品伊人久久久大香线蕉| 欧美一级片在线播放| 精品激情国产视频| 精品激情国产视频| 国产精品自产拍在线观看中文| 国产区精品视频| 在线观看国产成人av片| 欧美性xxxxxxx| 久久精品在线视频| 北条麻妃久久精品| 欧洲中文字幕国产精品| 欧美二区乱c黑人| 久99九色视频在线观看| 日本一区二区在线免费播放| 中文一区二区视频| 亚洲一级片在线看| 日本19禁啪啪免费观看www| 日韩欧美综合在线视频| 在线观看精品自拍私拍| 国产国产精品人在线视| 成人精品在线观看| 日本欧美一二三区| 九九热r在线视频精品| 欧美极品美女视频网站在线观看免费| 久久久久久久电影一区| 国产精品99久久99久久久二8| 麻豆乱码国产一区二区三区| 中文国产亚洲喷潮| 97香蕉超级碰碰久久免费软件| 日韩视频免费大全中文字幕| 国产精品视频男人的天堂| 亚洲国产精品推荐| 日本韩国在线不卡| 久久久噜噜噜久久| 久久精品视频播放| 欧美精品精品精品精品免费| 欧美中文字幕精品| 欧美一二三视频| 亚洲国产精品网站| 欧美猛少妇色xxxxx| 国产精品一区二区久久精品| 97色伦亚洲国产| 成人激情电影一区二区| 日韩高清av一区二区三区| 一区二区三区视频免费在线观看| 欧美一级视频一区二区| 日韩免费在线免费观看| 国产亚洲综合久久| 亚洲精品国产精品自产a区红杏吧| 欧美大片免费观看| 中文字幕欧美视频在线| 国产精品国产亚洲伊人久久| 国产精品吹潮在线观看| 国产欧美一区二区三区在线看| 国产国语videosex另类| 成人性生交大片免费观看嘿嘿视频| 国产精品女主播| 亚洲欧洲在线视频| 成人精品一区二区三区电影黑人| 1769国内精品视频在线播放| 国产亚洲日本欧美韩国| 91色视频在线导航| 国产精品女人网站| 2018中文字幕一区二区三区| 欧美性jizz18性欧美| 在线日韩中文字幕| 欧亚精品中文字幕| 九色精品免费永久在线| 精品日本美女福利在线观看| 日韩av中文字幕在线播放| 成人激情免费在线| 日韩国产欧美精品一区二区三区| 久久精品91久久久久久再现| 色老头一区二区三区| 欧美亚洲一区在线| 欧美壮男野外gaytube| 美女少妇精品视频| 欧美日韩国产精品一区二区不卡中文| 国产这里只有精品| 午夜精品福利视频| 最近2019中文字幕第三页视频| 国产精品激情av在线播放| 两个人的视频www国产精品| 日韩免费av在线| 欧美精品少妇videofree| 亚洲一区亚洲二区| 一区二区成人av| 91精品中文在线| 成人在线一区二区| 国产日本欧美一区| 草民午夜欧美限制a级福利片| 亚洲性夜色噜噜噜7777| 国产日本欧美在线观看| 夜夜狂射影院欧美极品| 777国产偷窥盗摄精品视频| 欧美成人午夜影院| 久久精品色欧美aⅴ一区二区| 国产欧美亚洲视频| 这里只有精品在线观看| 性色av一区二区三区| 久久视频国产精品免费视频在线| 欧美一级大片在线免费观看| 国产91精品久久久久久久| 国产精品看片资源| 日韩av影片在线观看| 日韩欧美国产一区二区| 国产一区二区香蕉| 久久露脸国产精品| 国产91ⅴ在线精品免费观看| 亚洲激情电影中文字幕| 国产日韩欧美在线| 欧美黑人xxxx| 亚洲在线免费看| 国产成人一区三区| 91亚洲一区精品| 欧美精品久久久久久久久久| 国产日韩在线亚洲字幕中文| 国产亚洲美女久久| 国产精品亚洲аv天堂网| 色婷婷综合成人av| 国产精品电影网| 亚洲福利视频网站| 国产va免费精品高清在线观看| 欧美成人合集magnet| 亚洲片国产一区一级在线观看| 国产精品福利网| 国产精品热视频|