前言git
在職場中,確立自身的技術水平很重要,由於,若是你被標記成了技術菜鳥,那麼你的工做一旦作快了,你們就會一致的認爲這個任務比較簡單;若是你未如期完成,則會被各類明嘲暗諷,你不但沒法得到合理的表揚,還會無故被迫接受攻擊。程序員
可是,若是你被標記成了技術高手,那麼你就算任務延期也會被理解,由於,他們會認爲你當前的任務太難了。並且,即使你有些性格缺陷,你們也是會接受你,他們會認爲這是你的特色。github
因此,進入新的工做崗位,第一件事是確立自身的技術水平,這會讓你省卻不少沒必要要的麻煩,會讓你在公司工做時,保持比較良好的狀態,進而延長你在公司任職的時間。api
那麼這些與內存有什麼關係呢?由於就是會有些人,會以你不瞭解【他們的】內存來攻擊你的技術水平。 由於在職場生存,除了不停的提高本身之外,仍是要關注周圍同事對你的見解,若是有人以一些你不瞭解的技術問題來否認你的技術水平,這就會很影響你在其餘同事心中的形象,從而影響你在職場中創建的技術水平的等級,這會讓你在將來的職場生涯中遇到更多的麻煩。安全
雖然,害人之心不可有,可是防人之心不可無,因此,咱們須要瞭解【他們的】內存,來保護本身,在被攻擊時,作更好的應對,甚至反擊。函數
託管內存與非託管內存工具
託管內存學習
C#語言開發的程序所使用的內存,咱們稱之爲託管內存。那麼什麼是託管內存呢?咱們能夠先理解爲,C#專用內存;即當C#的程序運行起來,會向電腦內存申請一塊專用的內存區,而這塊內存區,就叫作託管內存。編碼
在C#語言開發的程序中,咱們所聲明的變量,不管是常量,還變量,都在這塊內存中。即,咱們聲明一個int k或是聲明一個對象 new Class,他們都是在這塊內存中的。spa
而這塊內存(託管內存),它很特別,它自身是帶管理功能的,即,它本身會判斷,你聲明的內存還用不用,不用他就給回收了。
既然是管理,那就確定有個管理工具,那麼,託管內存的管理工具是什麼呢?
GC——控制系統垃圾回收器,這個就是託管內存的管理工具了,他是專門管理內存回收的,這裏就不過多的講解GC了,有興趣的朋友能夠參考下面的網址。
參考網址:
非託管內存
既然,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
----------------------------------------------------------------------------------------------------
注:此文章爲原創,任何形式的轉載都請聯繫做者得到受權並註明出處!
若您以爲這篇文章還不錯,請點擊下方的【推薦】,很是感謝!