.NET中栈和堆的比较(二)
盡管在.NET?framework下我們并不需要擔心內存管理和垃圾回收(Garbage?Collection),但是我們還是應該了解它們,以優化我們的應用程序。同時,還需要具備一些基礎的內存管理工作機制的知識,這樣能夠有助于解釋我們日常程序編寫中的變量的行為。在本文中我將講解我們必須要注意的方法傳參的行為。
在第一部分里我介紹了棧和堆的基本功能,還介紹到了在程序執行時值類型和引用類型是如何分配的,而且還談到了指針。
*?參數,大問題
這里有一個代碼執行時的詳細介紹,我們將深入第一部分出現的方法調用過程...
當我們調用一個方法時,會發生以下的事情:
1.方法執行時,首先在棧上為對象實例中的方法分配空間,然后將方法拷貝到棧上(此時的棧被稱為幀),但是該空間中只存放了執行方法的指令,并沒有方法內的數據項。
2.方法的調用地址(或者說指針)被放置到棧上,一般來說是一個GOTO指令,使我們能夠在方法執行完成之后,知道回到哪個地方繼續執行程序。(最好能理解這一點,但并不是必須的,因為這并不會影響我們的編碼)
3.方法參數的分配和拷貝是需要空間的,這一點是我們需要進一步注意。
4.控制此時被傳遞到了幀上,然后線程開始執行我們的代碼。因此有另一個方法叫做"調用棧"。
示例代碼如下:
此時棧開起來是這樣的:
就像第一部分討論的那樣,放在棧上的參數是如何被處理的,需要看看它是值類型還是引用類型。值類型的值將被拷貝到棧上,而引用類型的引用(或者說指針)將被拷貝到棧上。
*?值類型傳遞
首先,當我們傳遞一個值類型參數時,棧上被分配好一個新的空間,然后該參數的值被拷貝到此空間中。
來看下面的方法:
方法Go()被放置到棧上,然后執行,整型變量"x"的值"5"被放置到棧頂空間中。
然后AddFive()方法被放置到棧頂上,接著方法的形參值被拷貝到棧頂,且該形參的值就是"x"的拷貝。
當AddFive()方法執行完成之后,線程就通過預先放置的指令返回到Go()方法的地址,然后從棧頂依次將變量pValue和方法AddFive()移除掉:
所以我們的代碼輸出的值是"5",對吧?這里的關鍵之處就在于任何傳入方法的值類型參數都是復制拷貝的,所以原始變量中的值是被保留下來而沒有被改變的。
必須注意的是,如果我們要將一個非常大的值類型數據(如數據量大的struct類型)入棧,它會占用非常大的內存空間,而且會占有過多的處理器周期來進行拷貝復制。棧并沒有無窮無盡的空間,它就像在水龍頭下盛水的杯子,隨時可能溢出。struct是一個能夠存放大量數據的值類型成員,我們必須小心地使用。
這里有一個存放大數據類型的struct:
public?struct?MyStruct
{
????long?a,?b,?c,?d,?e,?f,?g,?h,?i,?j,?k,?l,?m;
}
來看看當我們執行了Go()和DoSometing()方法時會發生什么:
public?void?Go()
{
????MyStruct?x?=?new?MyStruct();
????DoSomething(x);
}
public?void?DoSomething(MyStruct?pValue)
{
????//?DO?SOMETHING?HERE....
}
這將會非常的低效。想象我們要是傳遞2000次MyStruct,你就會明白程序是怎么癱瘓掉的了。
那么我們應該如何解決這個問題?可以通過下列方式來傳遞原始值的引用:
public?void?Go()
{
????MyStruct?x?=?new?MyStruct();
????DoSomething(ref?x);
}
public?struct?MyStruct
{
????long?a,?b,?c,?d,?e,?f,?g,?h,?i,?j,?k,?l,?m;
}
public?void?DoSomething(ref?MyStruct?pValue)
{
????//?DO?SOMETHING?HERE....
}
通過這種方式我們能夠提高內存中對象分配的效率。
唯一需要注意的是,在我們通過引用傳遞值類型時我們會修改該值類型的值,也就是說pValue值的改變會引起x值的改變。執行以下代碼,我們的結果會變成"123456",這是因為pValue實際指向的內存空間與x變量聲明的內存空間是一致的。
public?void?Go()
{
????MyStruct?x?=?new?MyStruct();
????x.a?=?5;
????DoSomething(ref?x);
????Console.WriteLine(x.a.ToString());
}
public?void?DoSomething(ref?MyStruct?pValue)
{
????pValue.a?=?12345;
}
*?引用類型傳遞
傳遞引用類型參數的情況類似于先前例子中通過引用來傳遞值類型的情況。
如果我們使用引用類型:
{
????public?int?MyValue;
}
然后調用Go()方法,MyInt對象將放置在堆上:
public void Go()
{
??? MyInt x = new MyInt();
}
如果我們執行下面的Go()方法:
public?void?Go()
{
????MyInt?x?=?new?MyInt();
????x.MyValue?=?2;
????DoSomething(x);
????Console.WriteLine(x.MyValue.ToString());
}
public?void?DoSomething(MyInt?pValue)
{
????pValue.MyValue?=?12345;
}
將發生這樣的事情...
1.方法Go()入棧
2.Go()方法中的變量x入棧
3.方法DoSomething()入棧
4.參數pValue入棧
5.x的值(MyInt對象的在棧中的指針地址)被拷貝到pValue中
因此,當我們通過MyInt類型的pValue來改變堆中MyInt對象的MyValue成員值后,接著又使用指向該對象的另一個引用x來獲取了其MyValue成員值,得到的值就變成了"12345"。
而更有趣的是,當我們通過引用來傳遞一個引用類型時,會發生什么?
讓我們來檢驗一下。假如我們有一個"Thing"類和兩個繼承于"Thing"的"Animal"和"Vegetable"?類:
{
}
public?class?Animal?:?Thing
{
????public?int?Weight;
}
public?class?Vegetable?:?Thing
{
????public?int?Length;
}
然后執行下面的Go()方法:
{
????Thing?x?=?new?Animal();
????Switcharoo(ref?x);
????Console.WriteLine(
??????"x?is?Animal????:???"
??????+?(x?is?Animal).ToString());
????Console.WriteLine(
????????"x?is?Vegetable?:???"
????????+?(x?is?Vegetable).ToString());
}
public?void?Switcharoo(ref?Thing?pValue)
{
????pValue?=?new?Vegetable();
}
變量x被返回為Vegetable類型。
x?is?Animal????:???False
x?is?Vegetable?:???True
讓我們來看看發生了什么:
1.Go()方法入棧
2.x指針入棧
3.Animal對象實例化到堆中
4.Switcharoo()方法入棧
5.pValue入棧且指向x
6.Vegetable對象實例化到堆中
7.x的值通過被指向Vegetable對象地址的pValue值所改變。
如果我們不使用Thing的引用,相反的,我們得到結果變量x將會是Animal類型的。
如果以上代碼對你來說沒有什么意義,那么請繼續看看我的文章中關于引用變量的介紹,這樣能夠對引用類型的變量是如何工作的會有一個更好的理解。
我們看到了內存是怎樣處理參數傳遞的,在系列的下一部分中,我們將看看棧中的引用變量發生了些什么,然后考慮當我們拷貝對象時是如何來解決某些問題的。??
轉載于:https://www.cnblogs.com/dancingfeather/archive/2008/06/07/1215556.html
總結
以上是生活随笔為你收集整理的.NET中栈和堆的比较(二)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Ajax原理详细说明
- 下一篇: .Net页面的生命周期(ZZ)