官术网_书友最值得收藏!

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字節。

主站蜘蛛池模板: 故城县| 沐川县| 南部县| 远安县| 亚东县| 红原县| 寻甸| 渝北区| 南康市| 望江县| 汽车| 怀安县| 罗甸县| 太原市| 青铜峡市| 德钦县| 南澳县| 普安县| 乐至县| 石城县| 兴安盟| 洪湖市| 池州市| 南丰县| 沙洋县| 商南县| 新龙县| 六安市| 苏尼特右旗| 林芝县| 武平县| 平遥县| 吉首市| 磐石市| 页游| 沿河| 宜阳县| 古田县| 永州市| 新兴县| 渭南市|