(一)後台內存管理
1、值數據類型
Windows使用一個虛擬尋址系統,該系統把程序可用的內存地址映射到硬件內存中的實際地址,該任務由Windows在後台管理(32位每個進程可使用4GB虛擬內存,64位更多,這個內存包括可執行代碼和加載的DLL,以及程序運行時使用的變量內容)。
在處理器的虛擬內存中,有一個區域稱為棧。棧存儲不是對象成員的值數據類型。
釋放變量時,其順序總是與它們分配內存的順序相反,這就是棧的工作方式。
程序第一次運行時,棧指針指向為棧保留的內存塊末尾。棧實際上是向下填充的,即從高內存地址向低內存地址填充。當數據入棧後,棧指針就會隨之調整,以始終指向下一個空閒單元。
2、引用數據類型
托管堆使用一個方法(new運算符)來分配內存,再方法退出後很長一段時間存儲其中的數據仍可用。與棧不同,堆上的內存是向上分配的。
建立引用變量的過程要比建立值類型的過程更復雜,其不能避免性能的系統開銷。當一個引用變量超出作用域時,它會從棧中刪除,但引用的數據仍保留在堆中,一直到程序終止,會垃圾回收器刪除它為止,而只有在改數據不再被任何變量引用時,它才會被刪除。
3、垃圾回收器
垃圾回收器釋放了能釋放的所有對象,就會把其他對象移動回堆的端部,再次形成一個連續的塊。
堆的第一部分稱為第0代,創建的新對象會移動到這個部分。垃圾回收器每運行一次後保留的對象被壓縮後移動到下一代存放部分。
在.NET下,較大對象有自己的堆,稱為大象堆。使用大於85000個字節的對象時,它們就會放到這個特殊的堆上。
第二代和大象堆上的回收現在放在後台線程上進行。
GCSettings.LatencyMode屬性可以控制垃圾回收器進行垃圾回收的方式。
(二)釋放非托管資源
垃圾回收器不知道如何釋放非托管資源(文件句柄、網絡連接、數據庫連接),需要制定專門的規則,確保非托管資源在回收類的一個實例時釋放。
1、析構函數
析構函數的語法,沒有返回類型,不帶參數,沒有訪問修飾符,與類同名前面有一個波形符(~)。
class MyClass { ~MyClass(){ //析構函數 } }
C#析構函數無法確定何時執行。C#析構函數的實現會延遲對象最終從內存中刪除的時間。
2、IDisposable接口
C#中,推薦使用System.IDisposable接口替代析構函數。IDisposable接口聲明了一個Disposable()方法,它不帶參數,返回void。
class MyClass : IDisposable { public void Dispose() { //釋放 } }
調用Dispose()方法:
MyClass my = new MyClass(); //代碼 my.Dispose();
這種釋放方式,如果在過程代碼中拋出異常,Dispose()方法就沒有被調用,導致內存沒有被釋放掉。
MyClass my = new MyClass(); try { //代碼 } finally { my.Dispose(); }
通過以上調用方式,可以避免過程代碼拋出異常,導致內存沒被釋放掉。還可以使用using關鍵字來簡化調用,效果同上面一樣。
using (MyClass my = new MyClass()) { //代碼 }
(三)不安全代碼
1、用指針直接訪問內存
指針只是一個以與引用相同的方式存儲地址的變量。
(1)用unsafe關鍵字編寫不安全的代碼
不安全代碼所使用的關鍵字是unsafe。
unsafe class MyClass //不安全類 { unsafe public string Name { get; set; }//不安全屬性 unsafe void SayHi()//不安全方法 { Console.WriteLine("Hi!"+ Name); } void SayBay() { unsafe int* pAge;//不安全局部變量需要在不安全方法裡,這裡會報錯 Console.WriteLine("Bye!" + pAge); } }
(2)指針的語法
把代碼塊標記為unsafe後,使用以下語法聲明指針:
int* age;
聲明指針類型的變量後,就可以用與一般變量相同的方式使用它們,但首先需要學習另外兩個運算符:&表示“取地址”,*表示“獲取地址的內容”。
int x = 10; int* pX = &x; int y = *pX;
可以把指針聲明為任意一種值類型。
(3)將指針強制轉換為整數類型
由於指針實際上存儲了一個表示地址的整數,因此任何指針中的地址都可以和任何整數類型之間相互轉換。
int x = 100; ulong* pY = (ulong*)x;
需要注意的是,在32位系統上,一個地址占4個字節,把指針轉換為非uint、long或ulong時可能會導致溢出錯誤,64位系統一個地址占8個字節,把指針轉換為非ulong時會導致溢出錯誤。還要注意,指針的溢出無法通過checked關鍵字來檢查。因為.NET運行庫假定,如果使用指針就知道自己在做什麼,不必擔心溢出。
(4)指針類型之間的強制轉換
byte b = 10; byte* pB = &b; double* pD = (double*)pB;
(5)void指針
byte b = 10; byte* pB = &b; void* pV = (void*)pB;
void指針的主要作用是調用需要void*參數的API函數。
(6)指針算術的運算
可以給指針加減整數。給類型為T的指針加上數值X,其中指針的值為P,則得到的結果時P+X*(sizeof(T))。
byte b = 10; byte* pB = &b; pB--;
如果兩個指針類型相同,則可以把一個指針減去另一個指針,結果時一個long類型值為兩差值除類型所占字節數整除的結果
byte b1 = 10; byte* pB1 = &b1; byte b2= 11; byte* pB2 = &b2; long l = pB1 - pB2;
(7)sizeof運算符
使用sizeof運算符,它的參數是數據類型的名稱,返回該類型所占字符數。
int x = sizeof(int);//4
(8)結構指針:指針成員訪問運算符
結構指針的工作方式與預定義值類型的指針的工作方式完全相同。但是這有一個條件:結構不能包含任何引用類型,因為指針不能指向任何引用類型。
MyStruct* pStruct; MyStruct myStruct=new MyStruct(); pStruct= &myStruct; //通過指針訪問結構成員值 (*pStruct).X = 4; //另一種語法 *pStruct->X = 4;
(9)類成員指針
不能創建指向類的指針,這是因為垃圾回收期不維護關於指針的任何信息,只維護關於引用的信息,而在垃圾回收的過程中堆會被移動,這樣就會導致指針指向錯誤,為了解決這個問題需要使用fixed關鍵字,這樣告訴垃圾回收器,不移動這些對象。
MyClass myClass = new MyClass(); fixed (double* pX = &(myClass.X))//多個這樣的指針可以在代碼塊之前放置多條 fixed (long* pX = &(myClass.Y), pZ = &(myClass.Z))//指針類型相同時可以在一個括號內聲明 { fixed (long* pW&(myClass.W))//嵌套聲明 { } }
2、使用指針優化性能
1、創建基於棧的數組
指針的一個主要應用領域:在棧中創建高性能、低系統開銷的數組。為了創建一個高性能數組,需要使用另一個關鍵字:stackalloc。stackalloc命令提示.NET運行庫在棧上分配一定量的內存(數據類型所占字節數乘以項數)。在調用stackalloc命令時,需要提供要存儲的數據類型(必須是值類型)、需要存儲的數據項數。
decimal* pDecimal = stackalloc decimal[10];
項數還可以是一個變量:
int size = 5; decimal* pDecimal = stackalloc decimal[size];
stackalloc總是返回分配數據類型的指針,它指向新分配的內存塊的頂部。
要訪問數組的下一個元素,可以使用指針算法。用表達式*(pDecimal+X)訪問數組中下標為X的元素。
*pDecimal = 1;//數組第1項 *(pDecimal + 1) = 2;//數組第2項 C#還定義了另一種方法來訪問數組,與正常的數組訪問方式相同。 pDecimal[0] = 1;//等同與*pDecimal = 1; pDecimal[1] = 2; //等同與*(pDecimal + 1) = 2;
需要注意的是,當使用指針時編譯器無法檢查變量,這個時候當訪問項數超出分配的項數時會在運行時拋出異常。
pDecimal[20] = 21;
使用指針在獲得高性能的同時,也會付出一些代價:需要確保自己知道在做什麼,否則就會拋出非常古怪的運行錯誤。