作者:kiba518
連結:https://www.cnblogs.com/kiba/p/10971744.html
前言
在職場中,確立自身的技術水平很重要,因為,如果你被標記成了技術菜鳥,那麼你的工作一旦做快了,大家就會一致的認為這個任務比較簡單;如果你未如期完成,則會被各種明嘲暗諷,你不但無法獲得合理的表揚,還會無端被迫接受攻擊。
但是,如果你被標記成了技術高手,那麼你就算任務延期也會被理解,因為,他們會認為你當前的任務太難了。而且,即便你有些性格缺陷,大家也是會接受你,他們會認為這是你的特點。
所以,進入新的工作崗位,第一件事是確立自身的技術水平,這會讓你省卻很多不必要的麻煩,會讓你在公司工作時,保持比較良好的狀態,進而延長你在公司任職的時間。
那麼這些與記憶體有什麼關係呢?因為就是會有些人,會以你不瞭解【他們的】記憶體來攻擊你的技術水平。因為在職場生存,除了不停的提升自己以外,還是要關註周圍同事對你的看法,如果有人以一些你不瞭解的技術問題來否定你的技術水平,這就會很影響你在其他同事心中的形象,從而影響你在職場中建立的技術水平的等級,這會讓你在未來的職場生涯中遇到更多的麻煩。
雖然,害人之心不可有,但是防人之心不可無,所以,我們需要瞭解【他們的】記憶體,來保護自己,在被攻擊時,做更好的應對,甚至反擊。
託管記憶體與非託管記憶體
託管記憶體
C#語言開發的程式所使用的記憶體,我們稱之為託管記憶體。那麼什麼是託管記憶體呢?我們可以先理解為,C#專用記憶體;即當C#的程式執行起來,會向電腦記憶體申請一塊專用的記憶體區,而這塊記憶體區,就叫做託管記憶體。
在C#語言開發的程式中,我們所宣告的變數,不論是常量,還變數,都在這塊記憶體中。即,我們宣告一個int k或是宣告一個物件 new Class,他們都是在這塊記憶體中的。
而這塊記憶體(託管記憶體),它很特別,它自身是帶管理功能的,即,它自己會判斷,你宣告的記憶體還用不用,不用他就給回收了。
既然是管理,那就肯定有個管理工具,那麼,託管記憶體的管理工具是什麼呢?
GC——控制系統垃圾回收器,這個就是託管記憶體的管理工具了,他是專門管理記憶體回收的,這裡就不過多的講解GC了,有興趣的朋友可以參考下麵的網址。
參考網址:
GC——控制系統垃圾回收器
https://docs.microsoft.com/zh-cn/dotnet/api/system.gc
弱取用 WeakReference
https://docs.microsoft.com/en-us/dotnet/api/system.weakreference
非託管記憶體
既然,C#語言開發的程式所使用的記憶體,都叫託管記憶體,那麼非託管記憶體自然就是C#程式不使用的記憶體了。
那麼,C#程式不使用的記憶體,有什麼用呢?我們為什麼要學習呢?
因為,很多語言並不像C#這麼優秀,有專門的記憶體管理機制,比如C++;所以,他們的變數和常量都是儲存在非託管記憶體區的(對於很多語言而言,並沒有託管記憶體和非託管記憶體之分,他們就一個記憶體,在記憶體中找個地址,然後儲存資料)。
所以,當我們在做專案遇到要和其他語言進行互動時,就要接觸非託管記憶體了,因為很多時候,我們需要從非託管記憶體中獲取一些的變數,或者向非託管記憶體中寫入一些資料供其他語言呼叫。
因此,從理論上來講,C#語言對記憶體的管理是最複雜的,遠大於C++,因為它不僅自己開闢了一塊記憶體專區,同時又兼顧著控制專區外的記憶體。
下圖為託管記憶體與非託管記憶體的關係。
安全程式碼與非安全程式碼
安全程式碼
C#的安全程式碼就是C#日常寫的程式碼,其特點就是程式碼中宣告的變數都在託管記憶體;而之所以叫安全程式碼,則是因為記憶體全部託管給了記憶體管理器,不存在記憶體洩漏的問題(當然,這是理論上,實際情況某些微軟的控制元件還是存在記憶體洩漏的問題,相信一定有人遇到過,不過99%的情況下是沒問題的)。
非安全程式碼
非安全程式碼顯然是與安全程式碼相對的,即非安全程式碼的變數所使用的記憶體都在非託管記憶體區。
因為常規狀態下我們寫的程式碼都是安全程式碼,所以想寫非安全程式碼一定要加個特殊標記,那就是unsafe。
unsafe
{
}
如上述程式碼,在unsafe的區域內,我們就可以編寫非安全程式碼。
但C#專案在預設的情況下是不支援非安全程式碼的,即當我們嘗試些unsafe時,編譯器會報錯。為什麼不預設不允許我們使用非安全程式碼呢?很簡單因為它不安全嘛。
想啟用C#的非安全程式碼設定也很簡單,右鍵專案—屬性—生成,如下圖所示:
預設情況下,【允許不安全程式碼】是非勾選狀態;當我們勾選上之後,編譯器就允許我們使用unsafe了。
那麼,在unsafe區間如何控制非託管區域的記憶體呢?
這就需要使用到指標了,下麵我們講一下C#中的指標。
註意:非安全程式碼並不是C#的主要功能,而是為了相容其他使用非託管記憶體的語言而存在的,所以即便你不瞭解也並不會影響你的技術水平,但在職場中,這塊的內容非常容易成為菜鳥攻擊你的利器,所以學會它是職場生存的重要手段之一。
指標(Pointer)與控制代碼(IntPtr)
作為C#開發,我們要知道【宏】和【指標】會嚴重擾亂程式碼的脈絡,在開發中一定要儘量避免使用。
比如,你定義了一個Void*的指標,那Void*到底是個什麼東西啊!沒人知道,因為它什麼都能指向,很明顯,這嚴重的影響了程式碼的正常閱讀,因為我需要讀到Void*的時候,還有調查下它是個什麼東西;但我們又不是在看論文,看到特有名詞還得查一下他的含義,這簡直太荒唐了。
但在職場中,這些我們要儘量避免使用的東西,卻是最被經常談論的知識點,因為現在任何大學都會教C語言,所以,不論你的同事是程式員還是非技術人員,他們都多少聽過指標。而且【不會指標就不能算好程式員】幾乎已經是一個職場準則了。
因此,儘管C#開發不用這部分內容,也一定要瞭解起來,不能授人以柄不是嘛。
指標(Pointer)
指標簡單來說就是指向一塊記憶體的記憶體,我們可以透過指標指向的記憶體地址找到變數的值,並且改變它。
在C#中,我們也是可以定義指標的,不過那需要在非安全程式碼內定義;因為指標直接從記憶體中獲取地址的,也就是說,它並不是透過C#的記憶體管理工具來開闢記憶體的,所以,指標申請的這塊記憶體並不在託管程式碼的記憶體區中,那麼,很自然的,這塊記憶體就在非託管程式碼的記憶體區中了。
下麵我們先看這樣一段程式碼,來瞭解一下指標:
string str = "I am Kiba518!";
int strlen = str.Length;
IntPtr sptr = MarshalHelper.StringToIntPtr(str);
unsafe
{
char* src = (char*)sptr.ToPointer();
//Console.WriteLine("地址" + (&src;)); //這樣寫會報錯,C#並不支援這樣取指標地址
for (int i = 0; i <= strlen; i++)
{
Console.Write(src[i]);
src[i] = '0';
}
Console.WriteLine();
Console.WriteLine("========不安全程式碼改值=========");
for (int i = 0; i <= strlen; i++)
{
Console.Write(src[i]);
}
}
Console.ReadKey();
上述程式碼非常簡單,我先將字串傳送給MarshalHelper幫助類轉換成控制代碼(MarshalHelper中會開闢一個非託管區記憶體空間,然後把託管區的字串str的值賦值到這個非託管區記憶體,再生成一個指標指向這塊記憶體,最後在將這個指標轉換成IntPtr控制代碼,當然描述起來很複雜其實也就一句話Marshal.StringToHGlobalAnsi(str))然後呼叫轉換出來的控制代碼的ToPointer方法獲取到指標,接著在在非全程式碼區域使用指標輸出它的內容,再修改該它的值,最後將修改後值的指標內容打印出來。
PS:程式碼中的MarshalHelper是我封裝的一個類,用於處理型別與IntPtr的轉換,下方github中有該類程式碼。
其實指標在C#中有意義的功能就只剩下記憶體偏移量調整了,但實際開發中,C#專案是不需要做記憶體偏移量調整這種操作的。所以,純C#專案幾乎可以說已經棄用指標了。
控制代碼(IntPtr)
控制代碼其實是一個指標的封裝,同樣的,它也不常用,因為C#專案中指標都被棄用了,那指標的封裝—控制代碼自然也被棄用了。
但總有特殊的地方會用到指標,比如呼叫C++動態庫之類的;所以微軟貼心的為我們做了個控制代碼,畢竟指標用起來太難受了。
控制代碼是一個結構體,簡單的來說,它是指標的一個封裝,是C#中指標的替代者,下麵我們看下控制代碼的定義。
從圖中我們可以看到,控制代碼IntPtrt裡包含建立指標,獲取指標長度,設定偏移量等等方法,並且為了編碼方便還宣告了些強制轉換的方法。
看了控制代碼的結構體定義,相信稍微有點基礎的人已經明白了,在C#中,微軟是希望拋棄指標而改用更優秀的控制代碼代替它的。
但我們還會發現,控制代碼裡還提供一個方法是ToPointer(),它的傳回型別是Void*,也就是說,我們還是可以從控制代碼裡拿到C++中的指標,既然,微軟期望在C#中不要使用指標,那為什麼還要提供這樣的方法呢?
這是因為,在專案開發中總是會有極特殊的情況,比如,你有一段C++寫的非常複雜、完美的函式,而將這個函式轉換成C#又及其耗時,那麼最簡單省力的方法就是直接在C#裡啟用指標進行移植。
也就是說,C#支援指標,其實是為了體現它的相容性,並不是提倡大家去使用指標。
記憶體釋放
我先看如下程式碼:
static void Main(string[] args)
{
int retNoFree = Int32ToIntPtr_NoFree();
IntPtr retNoFreeIP = new IntPtr(retNoFree);
int retFree = Int32ToIntPtr_Free();
IntPtr retFreeIP = new IntPtr(retFree);
new Task(() =>
{
int afterNoFree = MarshalHelper.IntPtrToInt32(retNoFreeIP);
Console.WriteLine("Int32ToIntPtr_NoFree-未釋放Intptr的執行緒取值" + afterNoFree);
int afterFree = MarshalHelper.IntPtrToInt32(retFreeIP);
Console.WriteLine("Int32ToIntPtr_Free-已釋放Intptr的執行緒取值" + afterFree);
}).Start();
Console.ReadKey();
}
static int Int32ToIntPtr_Free()
{
IntPtr pointerInt = new IntPtr();
int testint = 518;
pointerInt = MarshalHelper.Int32ToIntPtr(testint);
int testintT = MarshalHelper.IntPtrToInt32(pointerInt);
Console.WriteLine("Int32ToIntPtr_Free-取IntPtr的值" + testintT);
MarshalHelper.Free(pointerInt);
int testintT2 = (int)pointerInt;
return testintT2;
}
static int Int32ToIntPtr_NoFree()
{
IntPtr pointerInt = new IntPtr();
int testint = 518;
pointerInt = MarshalHelper.Int32ToIntPtr(testint);
int testintT = MarshalHelper.IntPtrToInt32(pointerInt);
Console.WriteLine("Int32ToIntPtr_NoFree-取IntPtr的值" + testintT);
int testintT2 = (int)pointerInt;
return testintT2;
}
程式碼中有兩個函式Int32ToIntPtr_Free和Int32ToIntPtr_NoFree,兩個函式都是將變數testint轉換成指標,然後傳回該指標的地址(int型別),區別是一個呼叫了MarshalHelper.Free(pointerInt)進行指標記憶體釋放,一個沒有呼叫。
兩個函式執行完成後,開啟執行緒,透過其傳回的指標的地址,在重新查詢指標對應的內容,結果如下圖:
從圖中我們可以看到,未進行Free的IntPtr,仍然可以透過指標地址獲取到他的內容,而已釋放的IntPtr,透過地址再獲取內容,則已經是其他內容了。
PS:在C#中指標的記憶體釋放需要 Marshal.FreeHGlobal(IntPtr)方法,同樣的我將其封裝到了MarshalHelper中了。
結語
在職場,我們需要防備的通常不是高手,而是菜鳥,所以我們必須要增加各種各樣的知識儲備來應對這些奇奇怪怪的事情。
到此,C#記憶體管理講解就結束了。
程式碼已經傳到Github上了,歡迎大家下載。
Github地址:https://github.com/kiba518/MarshalHelper
朋友會在“發現-看一看”看到你“在看”的內容