1. 寫在前面的
在前文中,我主要介紹了數組的一些相關知識,希望加深各位對Array的理解,不過,看過Ivony…同學的回複,我發覺自己離說透還有很大的距離,於是就有了下面的文章。在本文中,我也主要來圍繞Ivony…同學提出的幾點問題來作以說明,問題如下:
A、數組在託管堆內部是怎麼存放的?數組元素的位置是連續的嗎?
B、非零基數組可以和零基數群組轉換嗎?
C、int[]與System.Array的關係到底是同一類型?還是基類與衍生類別的關係?
D、ldelem不檢查下標越界嗎?
E、多維陣列可以和零基數群組轉換嗎?
F、Array.Copy和CopyTo與手動拷貝效能有多大差距?
G、數組的協變是怎麼做到的?
H、數組是如何?泛型介面(如IList<T>)的?
I、多維陣列每一維度長度必須相等嗎?必須零基嗎?
J、數組的Length屬性到底指示的是什嗎?
2. 數組記憶體詳解
在這裡,我們依然把數組分為零基數組和非零基數組來討論。
首先來看零基數組的記憶體配置,廢話少說,我們先來看測試代碼:
static unsafe void Main(string[] args){ int[] intArr = new int[3]; intArr[0] = 1; intArr[1] = 2; intArr[2] = 3;}
代碼本身很簡單,接下來逐步執行向下看,首先我們來查看一下原始碼的彙編代碼:
int[] intArr = new int[3];00000035 mov edx,3 0000003a mov ecx,61CD4192h 0000003f call FFFB2140 00000044 mov dword ptr [ebp-44h],eax 00000047 mov eax,dword ptr [ebp-44h] 0000004a mov dword ptr [ebp-40h],eax intArr[0] = 1;0000004d mov eax,dword ptr [ebp-40h] 00000050 cmp dword ptr [eax+4],0 00000054 ja 0000005B 00000056 call 624B6B29 0000005b mov dword ptr [eax+8],1 intArr[1] = 2;00000062 mov eax,dword ptr [ebp-40h] 00000065 cmp dword ptr [eax+4],1 00000069 ja 00000070 0000006b call 624B6B29 00000070 mov dword ptr [eax+0Ch],2 intArr[2] = 3;00000077 mov eax,dword ptr [ebp-40h] 0000007a cmp dword ptr [eax+4],2 0000007e ja 00000085 00000080 call 624B6B29 00000085 mov dword ptr [eax+10h],3 }
在這裡,我們就可以清晰地發現,在0x0000005b,0x00000070和0x00000085中,mov操作的目標地址之間是相隔4個Bytes的,也就是一個整數位。接下來我們來進一步證實。
當我們為數組分配過記憶體位址後,開啟即使視窗查看數組所在的記憶體位址。
接下來開啟記憶體視窗還查看0x015cc790記憶體塊的資料:
以上是對數組賦值前的情況,賦值後的記憶體資料如下:
在這裡可以更清晰地看出,數組元素之間差的正好是4個Bytes,也就是一個整數位。由此,我們可以得出結論。零基數組的元素在記憶體中是連續排布的。
接下來我們來看一下非零基數組:
由於空間所限,過程如上,就不再發,證明:
總之,當我們在託管堆中為數組分配記憶體時,數組佔據一段連續的記憶體空間。
我們知道,當我們在託管堆中初始化一個對象時,每個對象都需要維護一個指標,該指標的作用是指向下一塊空閑記憶體空間,由於對數組的操作經常是迴圈遍曆等操作,這樣如果把數組分配到一個連續的記憶體空間有一下兩個好處:
A. 減少記憶體片段
B. 節省記憶體,不需要維護指標
C. 基地址不需要發生變化,只需要改變位移量即可,在一定程度上也提高了訪問的效率。
接下來,我們還需要來補充一下數組在棧上分配記憶體的情況:
還記得上文中提到的這個關鍵字吧,stackalloc,就是他了。補充一下,在上文的回複中,有人問到說棧空間上分配的記憶體是不是也被記憶體回收行程回收?這裡的棧空間和C語言中的棧一樣,沒有記憶體回收行程,每個變數都有他自己的範圍,當出了範圍後,變數自動銷毀,具體的函數執行過程,請參看《深入理解電腦系統》。
3. 再論零基數組和非零基數組
我們先來看這樣一段代碼:
static void Main(string[] args){ int[,] intArr = (int[,])(Array.CreateInstance(typeof(Int32), new int[] { 3,4 }, new int[] { 1,1 })); intArr[2, 3] = 1;}
這段代碼沒有問題,我們將Array顯式地轉換成了強型別的二維數組,然後直接存取索引對其複製。
但是我們知道,對弱類型的Array而言,我們不能通過其下標訪問他的元素,而只能通過SetValue和GetValue來獲得值,但是我們看到SetValue和GetValue訪問和設定的值的類型都是Object,這就意味著我們需要對其進行一次裝箱或者拆箱。那麼我們有沒有辦法也產生一個強型別的非零基數組呢?
在上文中,我們提到過,.NET Framework的幾種數群組類型:
一維零基數組:System.Int32[]。一維非零基數組:System.Int32[*]。多維陣列:System.Int32[,]。
那麼也就是說,我們是否能通過這樣的代碼來把Array轉換成一維非零基數組呢?
static void Main(string[] args){ int[*] intArr = (int[*])(Array.CreateInstance(typeof(Int32), new int[] { 3}, new int[] { 1 }));}
事實證明是錯誤的。在CLR via C#中Jeffery有這樣一段話:
“C# does not allow you to declare a variable of type string[*],and therefore it is not possible to user C# syntax to access a single-dimensional ,non-zero-based array.”
這段話翻譯成中文的意思就是:C#不允許聲明一個string[*]類型的變數,因此,我們能夠使用C#文法來訪問一個非零基一維數組。
通過以上的解釋,我們也許又額外明白了一點,Array究竟是數群組類型,還是數群組類型的基類?
通過上面的一些代碼,我們不妨又把數組重新分類為“強型別數組”和“弱類型數組”。而Array就屬於弱類型數組。為什麼Array是所有數群組類型的基類,我沒想出辦法來如何證明,只是看到Jeffery說了這樣一句話:
“All Arrays are Implicitly Derived from System.Array”。
我想這句話可以說明問題了,不過還是希望各位大俠指點如果證明這一點。
未完持續…………