stackalloc命令指示.NET運行庫分配堆棧上壹定量的內存。在調用它時,需要為它提供兩條信息:
● 要存儲的數據類型
● 需要存儲的數據個數。
例如,分配足夠的內存,以存儲10個decimal數據,可以編寫下面的代碼:
decimal* pDecimals = stackalloc decimal [10];
註意,這個命令只是分配堆棧內存而已。它不會試圖把內存初始化為任何默認值,這正好符合我們的目的。因為這是壹個高性能的數組,給它不必要地初始化值會降低性能。
同樣,要存儲20個double數據,可以編寫下面的代碼:
double* pDoubles = stackalloc double [20];
雖然這行代碼指定把變量的個數存儲為壹個常數,但它是在運行時計算的壹個數字。所以可以把上面的示例寫為:
int size;
size = 20; // or some other value calculated at run-time
double* pDoubles = stackalloc double [size];
從這些代碼段中可以看出,stackalloc的語法有點不尋常。它的後面緊跟的是要存儲的數據類型名(該數據類型必須是壹個值類型),其後是把需要的變量個數放在方括號中。分配的字節數是變量個數乘以sizeof(數據類型)。在這裏,使用方括號表示這是壹個數組。如果給20個double數據分配存儲單元,就得到了壹個有20個元素的double數組,最簡單的數組類型可以是:逐個存儲元素的內存塊,如圖7-6所示。
圖 7-6
在圖7-6中,顯示了壹個由stackalloc返回的指針,stackalloc總是返回分配數據類型的指針,它指向新分配內存塊的頂部。要使用這個內存塊,可以取消對返回指針的引用。例如,給20個double數據分配內存後,把第壹個元素(數組中的元素0)設置為3.0,可以編寫下面的代碼:
double* pDoubles = stackalloc double [20];
*pDoubles = 3.0;
要訪問數組的下壹個元素,可以使用指針算法。如前所述,如果給壹個指針加1,它的值就會增加其數據類型的字節數。在本例中,就會把指針指向下壹個空閑存儲單元。因此可以把數組的第二個元素(數組中元素號為1)設置為8.4:
double* pDoubles = stackalloc double [20];
*pDoubles = 3.0;
*(pDoubles+1) = 8.4;
同樣,可以用表達式*(pDoubles+X)獲得數組中下標為X的元素。
這樣,就得到壹種訪問數組中元素的方式,但對於壹般目的,使用這種語法過於復雜。C#為此定義了另壹種語法。對指針應用方括號時,C#為方括號提供了壹種非常明確的含義。如果變量p是任意指針類型,X是壹個整數,表達式p[X]就被編譯器解釋為*(p+X),這適用於所有的指針,不僅僅是用stackalloc初始化的指針。利用這個簡捷的記號,就可以用壹種非常方便的方式訪問數組。實際上,訪問基於堆棧的壹維數組所使用的語法與訪問基於堆的、由System.Array類表示的數組是壹樣的:
double *pDoubles = stackalloc double [20];
pDoubles[0] = 3.0; // pDoubles[0] is the same as *pDoubles
pDoubles[1] = 8.4; // pDoubles[1] is the same as *(pDoubles+1)
註意:
把數組的語法應用於指針並不是新東西。自從開發出C和C++語言以來,它們就是這兩種語言的基礎部分。實際上,C++開發人員會把這裏用stackalloc獲得的、基於堆棧的數組完全等同於傳統的基於堆棧的C和C++數組。這個語法和指針與數組的鏈接方式是C語言在70年代後期流行起來的原因之壹,也是指針的使用成為C和C++中壹種大眾化編程技巧的主要原因。
高性能的數組可以用與壹般C#數組相同的方式訪問,但需要強調其中的壹個警告。在C#中,下面的代碼會拋出壹個異常:
double [] myDoubleArray = new double [20];
myDoubleArray[50] = 3.0;
拋出異常的原因很明顯。使用越界的下標來訪問數組:下標是50,但允許的最大值是19。但是,如果使用stackalloc聲明了壹個相同數組,對數組進行邊界檢查時,這個數組中沒有包裝任何對象,因此下面的代碼不會拋出異常:
double* pDoubles = stackalloc double [20];
pDoubles[50] = 3.0;
在這段代碼中,我們分配了足夠的內存來存儲20個double類型數據。接著把sizeof(double)存儲單元的起始位置設置為該存儲單元的起始位置加上50*sizeof(double)存儲單元,來保存雙精度值3.0。但這個存儲單元超出了剛才為double分配的內存區域。誰也不知道這個地址上存儲了什麽數據。最好是只使用某個當前未使用的內存,但所重寫的空間也有可能是堆棧上用於存儲其他變量或某個正在執行的方法的返回地址。因此,使用指針獲得高性能的同時,也會付出壹些代價:需要確保自己知道在做什麽,否則就會拋出非常古怪的運行時錯誤。
2. 示例QuickArray
下面用壹個stackalloc示例QuickArray來結束關於指針的討論。在這個示例中,程序僅要求用戶提供為數組分配的元素數。然後代碼使用stackalloc給long型數組分配壹定的存儲單元。這個數組的元素是從0開始的整數的平方,結果顯示在控制臺上:
using System;
namespace Wrox.ProCSharp.Chapter07
{
class MainEntryPoint
{
static unsafe void Main()
{
Console.Write("How big an array do you want? \n> ");
string userInput = Console.ReadLine();
uint size = uint.Parse(userInput);
long* pArray = stackalloc long [(int)size];
for (int i=0 ; i pArray = i*i;
for (int i=0 ; i Console.WriteLine("Element {0} = {1}", i, *(pArray+i));
}
}
}
運行這個示例,得到如下所示的結果:
QuickArray
How big an array do you want?
> 15
Element 0 = 0
Element 1 = 1
Element 2 = 4
Element 3 = 9
Element 4 = 16
Element 5 = 25
Element 6 = 36
Element 7 = 49
Element 8 = 64
Element 9 = 81
Element 10 = 100
Element 11 = 121
Element 12 = 144
Element 13 = 169
Element 14 = 196
7.4 小結
要想成為真正優秀的C#程序員,必須牢固掌握存儲單元和垃圾收集的工作原理。本文描述了CLR管理以及在堆和堆棧上分配內存的方式,討論了如何編寫正確釋放未托管資源的類,並介紹如何在C#中使用指針,這些都是很難理解的高級主題,初學者常常不能正確實現。
本文摘至清華大學出版社出版的Wrox紅皮書《C#高級編程(第3版)》,