1.4 數組、結構體
將數組和結構體放在一起討論,主要是因為它們都是復合數據類型。
1.4.1 數組
數組在C語言中是比較簡單的結構,談它之前我們先從一個問題開始。筆者學習C時就感到很奇怪,為什么數組的第一個元素的索引要從0開始,而不是1。后來看到Pascal語言可用索引1開始訪問第一個元素,就非常鄙視C語言的“非人性做法”。當打開這個黑匣子分析清楚后,才明白自己的無知,一切選擇都有原因。
讓我們先猜測數組的訪問方式。一個最基本的想法是,要訪問一個內存只要拿到其地址即可。如何獲取到第i個元素的地址?最直接的想法是,如果能拿到數組首部地址,加上相對偏移就能計算出第i個元素的地址。這個偏移比較好計算,因為每個元素大小一樣,用元素個數乘以元素大小就能獲得偏移量。圖1.48給出了數組的內存結構。

圖1.48
第1個元素的地址 = 首地址a
第2個元素的地址 = 首地址a + 1×元素大小
第3個元素的地址 = 首地址a + 2×元素大小
......
第i個元素的地址 =首地址a + (i – 1)×元素大小
這里是用1作為元素第一個編號,在計算地址時總要進行一次減法運算。現在應該發現C語言數組索引編號的奧妙了吧,奇怪的方式一定有其原因。如果從0開始編號,計算地址時就沒有了減法,用違背常規的方式換來了計算速度:
第0個元素的地址 = 首地址a
第1個元素的地址 = 首地址a + 1×元素大小
第2個元素的地址 = 首地址a + 2×元素大小
......
第i個元素的地址 =首地址a + i×元素大小
好了,按照我們的習慣,到了實證的階段。程序代碼如下:
int array[5]; void main(){ array[0] = 1; array[1] = 2; array[2] = 3; }
其反匯編如下:
array[0] = 1; mov dword ptr ds:[004174c4h], 1 array[1] = 2; mov dword ptr ds:[004174c8h], 2 array[2] = 3; mov dword ptr ds:[004174cch], 3
可知,數組的首部地址是004174c4h;第2個元素是004174c8h,比前一個增加了4字節;第三個是004174cch,離首部偏移了8字節,用監視器查看array[0]的地址正好是004174c4h。因為int是4字節,所以偏移量是按4的整數倍遞增的。
我們再來看數組是局部變量的情況,其代碼如下:
int array[5]; array[0] = 1; mov dword ptr [ebp-18h], 1 // mov dword ptr[ebp - 18 h + 0 * 4], 1 array[1] = 2; mov dword ptr [ebp-14h], 2 // mov dword ptr[ebp - 18 h + 1 * 4], 2 array[2] = 3; mov dword ptr [ebp-10h], 3 // mov dword ptr[ebp - 18 h + 2 * 4], 3
每行后添加的注釋等價代碼清晰地顯示了數組是按首部地址+偏移量的做法。這些代碼還看不出基于0索引的數組的優勢。就用下面一段典型的for循環訪問數組來實證這一優勢。
for(i = 0; i < 10; i++) ... a[i] = 1; 00412036 mov eax, dword ptr [ebp-8] 00412039 mov dword ptr [ebp+eax*4-38h], 1
其中,最后黑體所示的eax * 4正如前所示為偏移量計算:ebp–38h是a數組的首址,eax存儲的是i的值(請自己證實)。
從以上反匯編我們也理解了C語言為什么會發生數組越界錯誤,因它只是拿到首部地址然后加偏移量,如果索引值超出范圍,那么求得的元素地址也就超過了范圍。請大家自己實驗超越范圍的數組元素訪問,并查看其反匯編代碼。
1.4.2 結構體
相對數組而言,更自由和更復雜的數據結構是結構體。還是老規矩,大家先猜測它在內存中會是什么樣子。例如:
struct Person{ int age; int no; }
兩個整數成員分配8字節應該就可以了。而且為了不浪費,這兩個成員變量應該是連續的。如何訪問其中的成員變量呢?與數組的思路相仿,如果拿到結構體首部地址,然后求偏移量即可。偏移量如何計算?因為代碼是編譯器生成的,它自然知道哪個字段在哪個位置,如Person的no成員是在偏4字節的地方,因為第一個成員占了4字節。

圖1.49
下面用真實代碼實證:
Person p; p.age = 1; mov dword ptr [ebp-0Ch], 1 p.no = 2; mov dword ptr [ebp-8], 2
從反匯編我們能得出其內存結構,見圖1.49。
第一條mov指令對ebp–0ch地址賦值。第二條指令對ebp–8地址賦值,該地址正好比ebp–0ch大4字節,說明賦值給了結構體首部偏4字節的no成員:ebp–8=ebp–0ch(結構體的首部地址)+4。
用監視窗體來證明分析,見圖1.50。可知,age的地址&p.age和ebp–0ch相等,為0x0012ff5c;no的地址&p.no與ebp–8相等,為0x0012ff60。

圖1.50
我們再來看一段代碼的編譯結果,并將版本改為Release版本,且關掉優化選項(在“項目屬性”中的“配置屬性→C/C++→優化→優化”選項設定為禁用)。該效果在VC 6.0中的Debug版本中可以查看到。
int no; int age; age = 1; mov dword ptr [ebp-8], 1 no = 2; mov dword ptr [ebp-4], 2
在該編譯狀態下,將代碼改為如下:
Person p; p.age = 1; mov dword ptr [ebp-8], 1 p.no = 2; mov dword ptr [ebp-4], 2
我們發現,兩者的反匯編代碼一模一樣。換句話說,無法從反匯編確定某塊內存到底是結構體還是相互比鄰的局部變量。結構體并未在內存中有更多特殊性,沒有用一段內存(如標志位之類)來表示這是一個結構體,內存大小看來就是全部成員變量之和(其實未必,請看1.5節),不過是編譯器提供給我們的一個方便自動求取偏移量的方法:我們給定要訪問的成員,編譯器就能自動為我們確定出偏移的位置,如p.no就自動偏移4字節。