前言托管內(nèi)存與非托管內(nèi)存 托管內(nèi)存 C#語言開發(fā)的程序所使用的內(nèi)存,我們稱之為托管內(nèi)存。那么什么是托管內(nèi)存呢?我們可以先理解為,C#專用內(nèi)存;即當(dāng)C#的程序運(yùn)行起來,會向電腦內(nèi)存申請一塊專用的內(nèi)存區(qū),而這塊內(nèi)存區(qū),就叫做托管內(nèi)存。 在C#語言開發(fā)的程序中,我們所聲明的變量,不論是常量,還變量,都在這塊內(nèi)存中。即,我們聲明一個int k或是聲明一個對象 new Class,他們都是在這塊內(nèi)存中的。 而這塊內(nèi)存(托管內(nèi)存),它很特別,它自身是帶管理功能的,即,它自己會判斷,你聲明的內(nèi)存還用不用,不用他就給回收了。 既然是管理,那就肯定有個管理工具,那么,托管內(nèi)存的管理工具是什么呢? GC——控制系統(tǒng)垃圾回收器,這個就是托管內(nèi)存的管理工具了,他是專門管理內(nèi)存回收的,這里就不過多的講解GC了,有興趣的朋友可以參考下面的網(wǎng)址。 參考網(wǎng)址: GC——控制系統(tǒng)垃圾回收器 https://docs.microsoft.com/zh-cn/dotnet/api/system.gc 弱引用 WeakReference https://docs.microsoft.com/en-us/dotnet/api/system.weakreference 非托管內(nèi)存 既然,C#語言開發(fā)的程序所使用的內(nèi)存,都叫托管內(nèi)存,那么非托管內(nèi)存自然就是C#程序不使用的內(nèi)存了。 那么,C#程序不使用的內(nèi)存,有什么用呢?我們?yōu)槭裁匆獙W(xué)習(xí)呢? 因為,很多語言并不像C#這么優(yōu)秀,有專門的內(nèi)存管理機(jī)制,比如C++;所以,他們的變量和常量都是存儲在非托管內(nèi)存區(qū)的(對于很多語言而言,并沒有托管內(nèi)存和非托管內(nèi)存之分,他們就一個內(nèi)存,在內(nèi)存中找個地址,然后存儲數(shù)據(jù))。 所以,當(dāng)我們在做項目遇到要和其他語言進(jìn)行交互時,就要接觸非托管內(nèi)存了,因為很多時候,我們需要從非托管內(nèi)存中獲取一些的變量,或者向非托管內(nèi)存中寫入一些數(shù)據(jù)供其他語言調(diào)用。 因此,從理論上來講,C#語言對內(nèi)存的管理是最復(fù)雜的,遠(yuǎn)大于C++,因為它不僅自己開辟了一塊內(nèi)存專區(qū),同時又兼顧著控制專區(qū)外的內(nèi)存。 下圖為托管內(nèi)存與非托管內(nèi)存的關(guān)系。 安全代碼與非安全代碼安全代碼 C#的安全代碼就是C#日常寫的代碼,其特點(diǎn)就是代碼中聲明的變量都在托管內(nèi)存;而之所以叫安全代碼,則是因為內(nèi)存全部托管給了內(nèi)存管理器,不存在內(nèi)存泄漏的問題(當(dāng)然,這是理論上,實(shí)際情況某些微軟的控件還是存在內(nèi)存泄漏的問題,相信一定有人遇到過,不過99%的情況下是沒問題的)。 非安全代碼 非安全代碼顯然是與安全代碼相對的,即非安全代碼的變量所使用的內(nèi)存都在非托管內(nèi)存區(qū)。 因為常規(guī)狀態(tài)下我們寫的代碼都是安全代碼,所以想寫非安全代碼一定要加個特殊標(biāo)記,那就是unsafe。 unsafe如上述代碼,在unsafe的區(qū)域內(nèi),我們就可以編寫非安全代碼。 但C#項目在默認(rèn)的情況下是不支持非安全代碼的,即當(dāng)我們嘗試些unsafe時,編譯器會報錯。為什么不默認(rèn)不允許我們使用非安全代碼呢?很簡單因為它不安全嘛。 想啟用C#的非安全代碼設(shè)置也很簡單,右鍵項目—屬性—生成,如下圖所示: 默認(rèn)情況下,【允許不安全代碼】是非勾選狀態(tài);當(dāng)我們勾選上之后,編譯器就允許我們使用unsafe了。 那么,在unsafe區(qū)間如何控制非托管區(qū)域的內(nèi)存呢? 這就需要使用到指針了,下面我們講一下C#中的指針。 注意:非安全代碼并不是C#的主要功能,而是為了兼容其他使用非托管內(nèi)存的語言而存在的,所以即便你不了解也并不會影響你的技術(shù)水平,但在職場中,這塊的內(nèi)容非常容易成為菜鳥攻擊你的利器,所以學(xué)會它是職場生存的重要手段之一。 指針(Pointer)與句柄(IntPtr)作為C#開發(fā),我們要知道【宏】和【指針】會嚴(yán)重擾亂代碼的脈絡(luò),在開發(fā)中一定要盡量避免使用。 比如,你定義了一個Void*的指針,那Void*到底是個什么東西啊!沒人知道,因為它什么都能指向,很明顯,這嚴(yán)重的影響了代碼的正常閱讀,因為我需要讀到Void*的時候,還有調(diào)查下它是個什么東西;但我們又不是在看論文,看到特有名詞還得查一下他的含義,這簡直太荒唐了。 但在職場中,這些我們要盡量避免使用的東西,卻是最被經(jīng)常談?wù)摰闹R點(diǎn),因為現(xiàn)在任何大學(xué)都會教C語言,所以,不論你的同事是程序員還是非技術(shù)人員,他們都多少聽過指針。而且【不會指針就不能算好程序員】幾乎已經(jīng)是一個職場準(zhǔn)則了。 因此,盡管C#開發(fā)不用這部分內(nèi)容,也一定要了解起來,不能授人以柄不是嘛。 指針(Pointer) 指針簡單來說就是指向一塊內(nèi)存的內(nèi)存,我們可以通過指針指向的內(nèi)存地址找到變量的值,并且改變它。 在C#中,我們也是可以定義指針的,不過那需要在非安全代碼內(nèi)定義;因為指針直接從內(nèi)存中獲取地址的,也就是說,它并不是通過C#的內(nèi)存管理工具來開辟內(nèi)存的,所以,指針申請的這塊內(nèi)存并不在托管代碼的內(nèi)存區(qū)中,那么,很自然的,這塊內(nèi)存就在非托管代碼的內(nèi)存區(qū)中了。 下面我們先看這樣一段代碼,來了解一下指針: 上述代碼非常簡單,我先將字符串發(fā)送給MarshalHelper幫助類轉(zhuǎn)換成句柄(MarshalHelper中會開辟一個非托管區(qū)內(nèi)存空間,然后把托管區(qū)的字符串str的值賦值到這個非托管區(qū)內(nèi)存,再生成一個指針指向這塊內(nèi)存,最后在將這個指針轉(zhuǎn)換成IntPtr句柄,當(dāng)然描述起來很復(fù)雜其實(shí)也就一句話Marshal.StringToHGlobalAnsi(str))然后調(diào)用轉(zhuǎn)換出來的句柄的ToPointer方法獲取到指針,接著在在非全代碼區(qū)域使用指針輸出它的內(nèi)容,再修改該它的值,最后將修改后值的指針內(nèi)容打印出來。 PS:代碼中的MarshalHelper是我封裝的一個類,用于處理類型與IntPtr的轉(zhuǎn)換,下方github中有該類代碼。 其實(shí)指針在C#中有意義的功能就只剩下內(nèi)存偏移量調(diào)整了,但實(shí)際開發(fā)中,C#項目是不需要做內(nèi)存偏移量調(diào)整這種操作的。所以,純C#項目幾乎可以說已經(jīng)棄用指針了。 句柄(IntPtr) 句柄其實(shí)是一個指針的封裝,同樣的,它也不常用,因為C#項目中指針都被棄用了,那指針的封裝—句柄自然也被棄用了。 但總有特殊的地方會用到指針,比如調(diào)用C++動態(tài)庫之類的;所以微軟貼心的為我們做了個句柄,畢竟指針用起來太難受了。 句柄是一個結(jié)構(gòu)體,簡單的來說,它是指針的一個封裝,是C#中指針的替代者,下面我們看下句柄的定義。 從圖中我們可以看到,句柄IntPtrt里包含創(chuàng)建指針,獲取指針長度,設(shè)置偏移量等等方法,并且為了編碼方便還聲明了些強(qiáng)制轉(zhuǎn)換的方法。 看了句柄的結(jié)構(gòu)體定義,相信稍微有點(diǎn)基礎(chǔ)的人已經(jīng)明白了,在C#中,微軟是希望拋棄指針而改用更優(yōu)秀的句柄代替它的。 但我們還會發(fā)現(xiàn),句柄里還提供一個方法是ToPointer(),它的返回類型是Void*,也就是說,我們還是可以從句柄里拿到C++中的指針,既然,微軟期望在C#中不要使用指針,那為什么還要提供這樣的方法呢? 這是因為,在項目開發(fā)中總是會有極特殊的情況,比如,你有一段C++寫的非常復(fù)雜、完美的函數(shù),而將這個函數(shù)轉(zhuǎn)換成C#又及其耗時,那么最簡單省力的方法就是直接在C#里啟用指針進(jìn)行移植。 也就是說,C#支持指針,其實(shí)是為了體現(xiàn)它的兼容性,并不是提倡大家去使用指針。 內(nèi)存釋放 我先看如下代碼: static void Main(string[] args)代碼中有兩個函數(shù)Int32ToIntPtr_Free和Int32ToIntPtr_NoFree,兩個函數(shù)都是將變量testint轉(zhuǎn)換成指針,然后返回該指針的地址(int類型),區(qū)別是一個調(diào)用了MarshalHelper.Free(pointerInt)進(jìn)行指針內(nèi)存釋放,一個沒有調(diào)用。 兩個函數(shù)執(zhí)行完成后,開啟線程,通過其返回的指針的地址,在重新查找指針對應(yīng)的內(nèi)容,結(jié)果如下圖: 從圖中我們可以看到,未進(jìn)行Free的IntPtr,仍然可以通過指針地址獲取到他的內(nèi)容,而已釋放的IntPtr,通過地址再獲取內(nèi)容,則已經(jīng)是其他內(nèi)容了。 PS:在C#中指針的內(nèi)存釋放需要 Marshal.FreeHGlobal(IntPtr)方法,同樣的我將其封裝到了MarshalHelper中了。 結(jié)語在職場,我們需要防備的通常不是高手,而是菜鳥,所以我們必須要增加各種各樣的知識儲備來應(yīng)對這些奇奇怪怪的事情。 到此,C#內(nèi)存管理講解就結(jié)束了。 代碼已經(jīng)傳到Github上了,歡迎大家下載。 Github地址:https://github.com/kiba518/MarshalHelper |
|
|