C语言中数组的分配和访问

来源:互联网 发布:g71编程实例 编辑:程序博客网 时间:2024/05/21 15:00

C语言中的数组是一种将标量数据聚集成更大数据类型的方式。
C语言可以产生指向数组中元素的指针,并对这些指针进行运算。在机器代码中,这些指针会被翻译成地址计算。

1.基本原则

对于数据类型T 和整型常数N,声明如下:

T  A[N];

它有两个效果:
 1.在存储器中分配一个L*N字节的连续区域L=sizeof(T)】;
 2.引入了标识符A——可以用A作为指向数组开头的指针,若用xA表示数组的起始位置,则该指针的值就是xA。

可以用从0到(N-1)之间的整数索引i来访问数组元素,数组元素A[i]会被存放在地址为(xA+L*i)的地方。让我们来看看下面这样的声明:

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

这些声明产生的数组带下列参数:

数组 元素大小 总的大小 起始地址 元素i A 1 12 xA xA+i B 4 32 xB xB+4*i C 8 48 xC xC+8*i D 4 20 xD xD+4*i

【注:】B和D都是指针数组,在32位机器中,指针变量占用4个字节的内存空间
  
  IA32的存储器引用指令可以用来简化数组访问。假设E是一个int型的数组,我们要计算E[i],在此,E的地址存放在寄存器%edx中,而i存放在寄存器%ecx中。那么指令

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

会执行地址计算xE+4*i,读这个寄存器位置的值,并将结果存放到寄存器%eax中。允许的缩放因子1、2、4和8覆盖了所有基本简单数据类型的大小。

2.指针运算

  C语言允许对指针进行运算,而计算出来的值会根据该指针引用的数据类型的大小进行伸缩。
  如果p是一个指向类型为T的数据的指针,p的值为xp,那么表达式(p+i)的值为(xp+L*i),这里L是数据类型T的大小。【L=sizeof(T)】
  
  单操作数的操作符&和*可以产生指针和间接引用指针。即对于一个表示某个对象的表达式Expr,&Expr是给出该对象地址的一个指针;对于一个表示地址的表达式AddrExpr,*AddrExpr是给出该地址处的值。因此,表达式Expr与(*&Expr)是等价的
  数组引用A[i]等同于表达式*(A+i),它计算第i个数组元素的地址,然后访问这个存储器位置。
  
  扩展一下前面的例子,假设整型数组E的起始地址和整数索引i分别存放在寄存器%edx和%ecx中,结果存放在寄存器%eax中。

表达式 类型 值 汇编代码 E int* xE movl  %edx, %eax E[0] int M[xE] movl  (%edx), %eax E[i] int M[xE+4*i] movl  (%edx,%ecx,4), %eax &E[2] int* xE+8 leal  8(%edx), %eax E+i-1 int* xE+4*i-4 leal  -4(%edx,%ecx,4), %eax *(E+i-3) int M[xE+4*i-12] movl  -12(%edx,%ecx,4), %eax &E[i]-E int i movl  %ecx, %eax

  在上面例子中,leal指令用来产生地址,而movl用来引用存储器(除了第一种和最后一种情况,前者是复制一个地址,而后者是复制索引)。最后一个例子表明我们可以计算同一个数据结构中的两个指针之差,结果值是除以数据类型大小后的值

3.嵌套的数组

  当我们创建数组的数组时,数组分配和引用的一般原则也是成立的。

int A[5][3];

等价于下面的声明

typedef int row3_t[3];row3_t A[5];

数据类型row3_t被定义为一个3个整数的数组。数组A包含5个这样的元素,每个元素需要12个字节来存储3个整数。整个数组的大小是4*5*3=60字节。
  数组A还可以看成一个5行3列的二维数组,用A[0][0]到A[4][2]来引用。数组元素在存储器中按照“行优先”的顺序排列,这意味着第i行的所有元素可以写作A[i]。将A看作是一个有5个元素的数组,每个元素都是3个int的数组,首先是A[0],然后是A[1],以此类推。
  
  要访问多维数组的元素,编译器会以数组起始为基地址,(可能需要经过伸缩的)偏移量为索引,产生计算期望的元素的偏移量,然后使用MOV指令。一般地,对于一个数组声明如下:

T  D[R][C];

它的数组元素D[i][j]的存储器地址为

&D[i][j]=xD+L*(C*i+j)

上式中L是数据类型T以字节为单位的大小。考虑前面定义的5*3的整型数组A,假设xA、i和j分别位于相对于%ebp偏移量为8、12和16的地方;则可以用下面的代码将数组元素A[i][j]复制到寄存器%eax中:

// A at %ebp+8,i at %ebp+12,j at %ebp+16----------movl  12(%ebp),  %eax      //得到ileal  (%eax,%eax,2), %eax  //计算3*imovl  16(%ebp), %edx       //得到jsall  $2, %edx             //计算4*jaddl  8(%ebp), %edx        //计算(xA+4*j)movl  (%edx,%eax,4) %eax   //从M[xA+4*j+12*i]处读取

这段代码计算元素的地址为xA+4*j+12*i=xA+4*(3*i+j),它使用移位、加法和伸缩的组合来避免开销更大的乘法操作

4.定长数组

  C语言编译器能够优化定长多维数组上的操作代码

#define N 16typedef int fix_matrix[N][N];

【当程序要用一个常数作为数组的维度或者缓冲区的大小时,最好通过#define声明将这个常数与一个名字联系起来,然后在后面一直使用这个名字代替常数的数值】

int fix_matrixMul(fix_matrix A,fix_matrix B,int i,int k){    int j;    int result=0;    for(j=0;j<N;j++)    {        result+=A[i][j]*B[j][k];  }    return result;}           (1) 原始的C代码

上面该段代码通过C语言编译器产生汇编后再反汇编成C代码,如下所示

int fix_matrixMul(fix_matrix A,fix_matrix B,int i,int k){    int *Arow=&A[i][0];    int *Bptr=&B[0][k];    int result=0;    int j;    for(j=0;j!=N;j++)    {        result+=Arow[j]*(*Bptr);        Bptr+=N;  }    return result;}           (2) 优化过的C代码

由于循环只会访问矩阵A的i行元素,故它创建了一个局部指针变量Arow,提供对第i行的直接访问;同理对于矩阵B是从B[0][k]的地址开始,按照B[1][k]…B[N-1][k]的顺序访问元素,相邻之间间隔N*4(16*4=64)个字节,故程序可以通过指针变量Bptr来访问这些位置。

  下面是该循环的实际汇编代码,其中函数的4个形参额(Arow、Bptr、j和result)都存放在寄存器中。

// Arow in %esi,Bptr in %ecx,j in %edx,result in %ebx___________.L6:                            //loop:    movl  (%ecx),  %eax         //     get *Bptr    imull  (%esi,%edx,4),  %eax //     multiply by Arow[j]    addl  %eax,  %ebx           //     add to result    addl  $1,   %edx            //     increment j    addl  $64,  %ecx            //     add 64 to Bptr    cmpl  $16,  %edx            //     compare j:16    jne   .L6                   //     if !=,goto loop     

在循环中,寄存器%ecx是被增加64(第6行)。机器代码认为每个指针都指向的一个字节地址,因此在编译指针运算中,每次都应该增加底层数据类型的大小。

5.变长数组

  以前程序员需要变长数组时,不得不用malloc或calloc这样的函数为这些数组分配存储空间,用行优先索引将多维数组映射到一维的数组。ISO C99引入了一种能力,允许数组的维度是表达式,在数组被分配的时候才计算出来,并且最近的GCC版本支持ISO C99中关于变长数组的大部分规则。
  当一个数组被声明为int A[expr1][expr2],它可以作为一个局部变量,也可以作为一个函数的参数;当遇到这个声明时,通过对表达式expr1和expr2求值来确定数组的维度。

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

参数n必须在参数A[n][n]之前,这样函数就可以在遇到这个数组的时候计算出数组的维度。

  GCC为这个引用函数产生的代码如下:

// n at %ebp+8, A at %ebp+12, i at %ebp+16,j at %ebp+20____________movl 8(%ebp),%eax       // Get nsall $2,%eax            // Compute 4*nmovl %eax,%edx          // Copy 4*nimull 16(%ebp),%edx     // Compute 4*n*imovl 20(%ebp),%eax      // Get jsall $2,%eax            // Compute 4*jaddl 12(%ebp),%eax      // Compute xA+4*jmovl (%eax,%edx),%eax   // Read from xA+4*(n*i+j)

这段代码计算元素A[i][j]的地址为xA+4*(n*i+j)。这个计算类似于定长数组的地址计算,不同点是

1)由于加上了参数n,参数在栈上的地址移动了;
2)用乘法指令计算n*i(第4行),而不是用leal指令计算3*i。

动态的版本必须用乘法指令对i伸展n倍,而不能用一系列的移位和加法

  在一个循环中引用变长数组时,编译器常常可以利用访问模式的规律性来优化索引的计算。

int var_prod_ele(int n,int A[n][n],int B[n][n],int i,int k){    int j;    int result=0;    for(j=0;j<n;j++)        result+=A[i][j]*B[j][k];    return result;}

上述代码计算变长数组的矩阵乘积(A的第i行与B的第k列元素的乘积之和)。编译器执行的优化类似于对定长数组的优化,它对应的汇编代码如下

// n stored at %ebp+8//  Arow in %esi,Bptr in %ecx,j in %edx ,result in %ebx,%edi holds 4*n____________.L30:                        //Loop:  movl (%ecx),%eax         //     Get *Bptr  imull (%esi,%edx,4),%eax //    Multiply by Arrow[j]  addl %eax,%ebx           //    Add to result  addl $1,%edx             //    Increment j  addl %edi,%ecx           //    Add 4*n to Bptr  cmpl %edx,8(%ebp)        //    Compare n:j  jg   .L30                //    If>,goto loop

  我们看到程序既使用了伸缩过的值4*n(寄存器%edi)来增加Bptr,也使用了存储在相对于%ebp偏移量为8处的n的实际值来检查循环的边界。C代码中并没有体现出需要这两个值,但由于指针运算的伸缩,才使用了这两个值。
  
  每次循环中,代码从存储器中取出n的值,检查循环是否终止(第7行)。这是一个寄存器溢出(register spilling)的例子:没有足够多的寄存器来保存需要的临时数据,故编译器必须把一些局部变量存放在存储器中。在本例中,编译器选择把n溢出,因为它是一个“只读”的值——在循环中不会改变它的值。因为IA32处理器的寄存器数量太少,必须常常将循环溢出到存储器中。通常,读存储器完成起来比写存储器要容易得多,故将只读变量溢出是比较合适的。

0 0