歡迎光臨
每天分享高質量文章

C# 記憶體管理

作者: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

已同步到看一看
赞(0)

分享創造快樂