1. 程式人生 > >深入理解計算機系統(3.8)------數組分配和訪問

深入理解計算機系統(3.8)------數組分配和訪問

2個 說明 add 如果 c++編譯 類型 操作 http 程序

  上一篇博客我們講解了匯編語言中過程(函數)的調用實現。理解數據如何在調用者和被調用者之間傳遞,以及在被調用者當中局部變量內存的分配以及釋放是最重要的。那麽這篇博客我們將講解數組的分配和訪問。

1、數組的基本原則

  我們知道數組是某種基本數據類型數據的集合,對於數據類型 T 和整型常數 N,數組的聲明如下:

T  A[N]

  上面的 A 稱為數組名稱。它有兩個效果:

  ①、它在存儲器中分配一個 L*N 字節的連續區域,這裏 L 是數據類型 T 的大小(單位為字節)

  ②、A 作為指向數組開頭的指針,如果分配的連續區域的起始地址為 xa,那麽這個指針的值就是xa

  即當我們用 A[i] 去讀取數組元素的時候,其實我們訪問的是 xa+i*sizeof(T)。sizeof(T)是獲得數據類型T的占用內存大小,以字節為單位,比如如果T為int,那麽sizeof(int)就是4。因為數組的下標是從0開始的,當 i等於0時,我們訪問的地址就是 xa

  比如對於如下數組聲明:

   char A[12];
   char *B[8];
   double C[6];
   double *D[5];

  我們可以得到如下信息:註意由於B和D都是聲明的數組,在IA32中,指針變量占用4個字節的內存空間。

  技術分享

  在比如如下代碼:

#include <stdio.h>

int main(){
   int a[10];
   int i ;
   for(i = 0 ; i < 10 ; i++){
   	printf("%d\n",&a[i]);
   } 
   printf("數組大小為:%d\n",sizeof(a)); 
   return 0;
}

  打印結果為:

  技術分享

  從上面的我們也可以看出來,起始地址為 6356736,即a[0]的地址,往後面訪問依次增加4個字節。

  在IA32中,存儲器引用指令可以用來簡化數組訪問。比如對於上面的 int a[10],我們想訪問 a[i],這時候 a 的地址存放在寄存器 %edx 中,而 i 存放在寄存器 %ecx 中。然後指令計算如下:

movl  (%edx,%ecx,4), %eax

  這會執行地址計算 xa+4i,讀取這個存儲器位置的值,並把結果存放在寄存器%eax中。

2、指針運算

  C語言允許對指針進行運算,而計算出來的值會根據該指針引用的數據類型的大小進行伸縮。

  也就是說,如果 P 是一個執行類型 T 的數據的指針,P 的值為 xp,那麽表達式P+i 的值為 xp+L*i,這裏 L 是數據類型T的大小。

  假設整型數組 E 的起始地址和整數索引 i 分別存放在寄存器 %edx 和 %ecx 中,下面是每個表達式的匯編代碼實現,結果存放在 %eax 中。

  技術分享

  上面例子中,leal 指令用來產生地址,而 movl 用來引用存儲器(除了第一種和最後一種情況,前者是復制一個地址,後者是復制索引);最後一個例子說明可以計算同一個數據類型結構中的兩個指針之差,結果值是除以數據類型大小後的值。

3、數組的嵌套

  也就是數組的數組,比如二維數組 int A[5][3]。這個時候上面所講的數組的分配和引用也是成立的。

  對於數組 int A[5][3],如下表示:

  技術分享

  我們可以將 A 看成是一個有 5 個元素的數組,而每個元素都是 3 個 int 類型的數組。

4、定長數組和變長數組

  要理解定長和變長數組,我們必須搞清楚一個概念,就是說這個“定”和“變”是針對什麽來說的。在這裏我們說,這兩個字是針對編譯器來說的,也就是說,如果在編譯時數組的長度確定,我們就稱為定長數組,反之則稱為變長數組。

  比如int A[10],就是一個定長數組,它的長度為10,它的長度在編譯時已經確定了,因為長度是一個常量。之前的C編譯器不允許在聲明數組時,將長度定義為一個變量,而只能是常量,不過當前的C/C++編譯器已經開始支持動態數組,但是C++的編譯器依然不支持方法參數。另外,C語言還提供了類似malloc和calloc這樣的函數動態的分配內存空間,我們可以將返回結果強轉為想要的數組類型。

  對於如下程序:

int main(){
    int a[5];
    int i,sum;
    for(i = 0 ; i < 5; i++){
        a[i] = i * 3;
    }
    for(i = 0 ; i < 5; i++){
        sum += a[i];
    } 
    return sum;
}

  我們加上 -O0 -S 變成匯編代碼:

main:
    pushl    %ebp
    movl    %esp, %ebp//到此準備好棧幀
    subl    $32, %esp//分配32個字節的空間
    leal    -20(%ebp), %edx//將幀指針減去20賦給%edx寄存器
    movl    $0, %eax//將%eax設置為0,這裏的%eax寄存器是重點
.L2:
    movl    %eax, (%edx)//將0放入幀指針減去20的位置?
    addl    $3, %eax//第一次循環時,%eax為3,對於i來說,%eax=(i+1)*3。
    addl    $4, %edx//將%edx加上4,第一次循環%edx指向幀指針-16的位置
    cmpl    $15, %eax//比較%eax和15?
    jne    .L2//如果不相等的話就回到L2
    movl    -20(%ebp), %eax//下面這五句指令已經出賣了leal指令,很明顯從-20到-4,就是數組五個元素存放的地方。下面的就不解釋了,直接依次相加然後返回結果。
    addl    -16(%ebp), %eax
    addl    -12(%ebp), %eax
    addl    -8(%ebp), %eax
    addl    -4(%ebp), %eax
    leave
    ret

  指令上面的註釋已經很清楚了,下面我們看看循環過程是怎麽計算的:

  技術分享

  看了這個圖相信各位更加清楚程序的意圖了,開始將%ebp減去20是為了依次給數組賦值。這裏編譯器用了非常變態的優化技巧,那就是編譯器發現了a[i+1] = a[i] + 3的規律,因此使用加法(將%eax不斷加3)代替了i*3的乘法操作,另外也使用了加法(即地址不斷加4,而不使用起始地址加上索引乘以4的方式)代替了數組元素地址計算過程中的乘法操作。而循環條件當中的i<5,也變成了3*i<15,而3*i又等於a[i],因此當整個數組當中循環的索引i,滿足a[i+1]=15(註意,在循環內的時候,%eax一直儲存著a[i+1]的值,除了剛開始的0)的時候,說明循環該結束了,也就是coml和jne指令所做的事。

  弄清楚了定長數組,下面我們在看看變長數組。在GCC版本支持的 ISO C99中,允許數組的維度是表達式,在數組被分配的時候才計算出來。比如下面這個函數:

int var_ele(int n,int A[n][n],int i,int j) 
{
	return A[i][j];
}

  產生的匯編代碼如下:

  技術分享

  如上圖所示,在計算元素 i,j的地址為xa+4(n*i+j)。這個計算類似於定長數組的地址計算,不同的是:

  ①、由於加上了參數n,參數在棧上的地址移動了

  ②、用了乘法指令計算n*i(第4行),而不是leal指令計算3i。

  因此引用變長數組只需要對定長數組做一點改動,動態的版本必須用乘法指令對i擴展n倍,而不能用一系列的移位和加法。在一些處理器中,乘法指令會消耗很長的指令周期,但是在這種情況下是不可避免的。

  

深入理解計算機系統(3.8)------數組分配和訪問