C Primer Plus 第10章 数组和指针 10.7 指针和多维数组

指针和多维数组有什么关系?为什么我们需要知道它们之间的关系?函数是通过指针来处理多维数组的,因此在使用这样的函数之前,您需要更多的了解指针。假设有如下的声明:

int  zippo[4][2] ;  /*整数数组的数组*/

数组名zippo同时也是数组首元素的地址。在本例中,zippo的首元素本身又是包含两个int的数组,因此zippo也是包含两个int的数组的地址。下面从指针的属性进一步分析:

**因为zippo是数组首元素的地址,所以zippo的值和&zippo[0]相同。另一方面,zippo[0]本身是包含两个整数的数组,因此zippo[0]的值同其首元素的地址&zippo[0][0]相同。简单地说,zippo[0]是一个整数大小对象的地址,而zippo是两个整数大小对象的地址。因为整数和两个整数组成的数组开始于同一个地址,因此zippo和zippo[0]具有相同的值 。

**对一个指针加1会对原来的数值加上一个对应类型大小的数值。在这方面zippo和zippo[0]是不一样的,zippo所指向对象的大小是两个int,而zippo[0]所指向对象的大小是一个int。因此zippo+1和zippo[0]+1的结果不同。gotop.gif

**对一个指针取值得到的是该指针所指向对象的数值。因为zippo[0]是其首元素zippo[0][0]的地址,所以*(zippo[0])代表存储在zippo[0][0]中的数值,即一个int数值。同样*zippo代表其首元素zippo[0]的值,但是zippo[0]本身就是一个int数的地址,即&zippo[0][0],因此*zippo是&zippo[0][0]。对这个表达式同时应用取值运算符将得到**zippo等价于*&zippo[0][0],后者简化后即为一个int数zippo[0][0]。简言之,zippo是地址的地址,需要两次取值才可以得到通常的数值。地址的地址或指针的指针是双重间接的典型例子。

显然,增加数组维度会增加指针的复杂度。

程序清单10.15

/*zippo1.c --有关zippo的信息*/
#include <stdio.h>
int main(void)
{
    int zippo[4][2]={{2,4},{6,8},{1,3},{5,7}};

    printf("zippo = %p,  zippo+1 = %p\n",
            zippo,       zippo+1);
    printf("zippo[0] = %p,  zippo[0]+1 = %p\n",
            zippo[0],       zippo[0]+1);
    printf("*zippo = %p,  *zippo+1 = %p\n",
            *zippo,       *zippo+1);
    printf("zippo[0][0] = %d\n",zippo[0][0]);
    printf("  *zippo[0] = %d\n",*zippo[0]);
    printf("    **zippo = %d\n",**zippo);
    printf("      zippo[2][1] = %d\n",zippo[2][1]);
    printf("*(*(zippo+2)+1)=%d\n",*(*(zippo+2)+1));
    return 0;
}

在一个系统上输出结果如下

zippo = 0022FF20,  zippo+1 = 0022FF28
zippo[0] = 0022FF20,  zippo[0]+1 = 0022FF24
*zippo = 0022FF20,  *zippo+1 = 0022FF24
zippo[0][0] = 2
  *zippo[0] = 2
    **zippo = 2
      zippo[2][1] = 3
*(*(zippo+2)+1)=3

输出显示出二维数组zippo的地址和一维数组zippo[0]的地址是相同的,均为相应的数组首元素的地址,它的值是和&zippo[0][0]相同的。

然而,差别也是有的,在我们系统上,int是4个字节长。前面我们讨论过,zippo[0]指向4字节长的数据对象,对zippo[0]加1导致它的值增加4。数组名zippo是包含两个int数的数组的地址,因此它指向8字节长的数据对象。所以,对zippo加1导致它的值增加8。

程序显示*zippo和zippo[0]是相同的,这点是正确的。另一方面,二维数组名必须再次取值才能取出数组中存储的数据

具体地:zippo[2][1]的等价指针符号表示为*(*(zippo+2)+1)。表10.2中分步建立了这个表达式:

分析*(*(zippo+2)+1)

zippo第1个大小 为2个int的元素的地址
zippo+2第3个大小为2个int的元素的地址
*(zippo+2)第3个元素,即包含2个int值的数组,因此也是其第1个元素(int值)的地址
*(zippo+2) +1包含2个Int值的数组的第2个元素(int值)的地址
*(*(zippo+2) +1)数组第3行第2个int的值(zippo[2][1])

当您正好有一个指向二维数组的指针并需要取值时,最好不要使用指针符号,而应当使用形式简单的数组符号。

10.7.1  指向多维数组的指针

如何声明指向二维数组的指针变量pz?例如,在编写处理像zippo这样的数组的函数时,就会用到这类指针。指向int的指针可以胜任吗?不可以。这种指针只是和zippo[0]兼容。因为它们都指向一个单个的int值。但是zippo是其首元素的地址,而该首元素又是包含两个int值的数组。因此,pz必须指向一个包含两个int值的数组,而不是指向一个单个的int值。下面是正确的代码:

int (*pz) [2] ;  //pz指向一个包含2个int值的数组

该语句表明pz是指向包含2个int值的数组的指针。为什么使用圆括号?因为表达式中[]的优先级高于*。因此,如果我们这样声明:

int * pax[2] ;

那么首先[]与pax结合,表示pax是包含两个某种元素的数组。然后和*结合,表示pax是两个指针组成的数组。最后,用int来定义,表示pax是由两个指向int值的指针构成的数组。这种声明会创建两个指向单个Int值的指针。程序清单10.16显示了如何使用指向二维数组的指针。

程序清单10.16  zippo2.c

/*zippo2.c  --通过一个指针变量获取有关zippo的信息*/
#include <stdio.h>
int main(void)
{
    int zippo[4][2]={{2,4},{6,8},{1,3},{5,7}};
    int (*pz)[2];
    pz=zippo;

    printf(" pz = %p,  pz+1 = %p\n",
             pz,       pz+1);
    printf(" pz[0] = %p,pz[0]+1 = %p\n",
             pz[0],     pz[0]+1);
    printf("  *pz = %p,*pz+1 = %p\n",
               *pz,     *pz+1);
    printf("pz[0][0] = %d\n",pz[0][0]);
    printf("  *pz[0] = %d\n",*pz[0]);
    printf("    **pz = %d\n",**pz);
    printf("      pz[2][1] = %d\n",pz[2][1]);
    printf("*(*(pz+2)+1) = %d\n",*(*(pz+2)+1));
    return 0;
}

输出结果如下:

 pz = 0022FF1C,  pz+1 = 0022FF24
 pz[0] = 0022FF1C,pz[0]+1 = 0022FF20
  *pz = 0022FF1C,*pz+1 = 0022FF20
pz[0][0] = 2
  *pz[0] = 2
    **pz = 2
      pz[2][1] = 3
*(*(pz+2)+1) = 3

不同的计算机得到的结果可能有些差别,但是相互关系是一样的。尽管pz是一个指针,而不是数组名,仍然可以使用pz[2][1]这样的符号。

更一般地,要表示单个元素,可以使用数组符号或指针符号:并且在这两种表示中即可以使用数组名也可以使用指针:

zippo[m][n] == *(*(zippo+m)+n)

pz[m][n] == *(*(pz+m)+n)

10.7.2  指针兼容性

指针之间的赋值规则比数值类型的赋值更严格。例如,您可以不需要进行类型转换就直接把一个Int数值赋给一个double类型的变量。但对于指针来说这样的赋值是不允许的。

这些规则也适用于更复杂的类型。假设有如下声明:

int * pt;
int (*pa)[3];
int ar1[2][3];
int ar2[3][2];
int **px;  //指针的指针

那么,有如下结论:

pt = &ar1[0][0];  //都指向 int
pt = ar1[0];      //都指向 int
pt = ar1;         //非法
pa = ar1;         //都指向int [3]
pa = ar2;         //非法
p2 = &pt          //都指向int *
*p2 = ar2[0]      //都指向int
p2 = ar2;         //非法

请注意,上面的非法赋值都包含着两个不指向同一类型的指针。例如,Pt指向一个int数值,但是ar1是指向由3个int值构成的数组。同样,pa指向由3个int值构成的数组,因此它与ar1的类型一致,但是和ar2的类型不一致,因为ar2指向由2个int值构成的数组。

后面的两个例子比较费解。变量p2是指向int的指针的指针,然而,ar2是指向由2个int值构成的数组的指针(简单一些说是指向int[2]的指针)。因此,p2和ar2的类型不同,不能把ar2的值赋给p2。但是*p2的类型是指向int的指针,所以它和ar2[0]是兼容的。前面讲过,ar2[0]是指向其首元素ar2[0][0]的指针,因此ar2[0]也是指向int的指针。

一般地,多重间接运算不容易理解。例如,考虑下面这段代码:

int *p1;
const int *p2;
const int **p2;
p1=p2;  //非法,把const指针赋给非const指针
p2=p1;  //合法,把非const指针赋给const指针
pp2=&p1;//非法,把非const指针赋给const指针

正如前面所提到的,把const指针赋给非const指针是错误的,因为您可能会使用新指针来改变const数据。但是把非const指针赋给const指针是允许的。这们的赋值有一个前提:只进行一层间接运算:

p2=p1;  //合法,把非const指针赋给const指针

在进行两层间接运算时,这样的赋值不再安全。如果允许这样赋值,可能会产生如下的问题:

const int **pp2;
int *p1;
const int n=13;
pp2=&p1;  //不允许,我们假设允许
*pp2=&n;  //合法,两者都是const,但同时会使p1指向n
*p1=10;  //合法,但这将改变const n的值

10.7.3  函数和多维数组

如果需要编写一个处理二维数组的函数,首先需要很好的理解指针以便正确声明函数的参数。在函数体内通常可以使用数组符号来避免使用指针 。

下面我们编写一个处理二维数组的函数,一种方法是把处理一维数组的函数应用到二维数组的每一行上,也就是如下所示这样处理:

int junk[3][4]={{2,4,5,8},{3,5,6,9},{12,10,8,6}};
int i,j;
int total=0;
for(i=0;i<3;i++)
  total+=sum(junk[i],4);  //junk[i]是一维数组

如果junk是二维数组,junk[i]就是一维数组,可以把它看作是二维数组的一行。函数sum()计算二维数组每行的和,然后由for循环把这些和加起来得到“总和”。

然而,使用这种方法得不到行列信息。要具有行列信息,需要恰当地声明形参变量以便于函数能够正确的传递数组。在本例中,数组junk是3行4列的int数组。如前面所讨论的,这表明junk是指向由4个int值构成的数组的指针。声明此类函数参量的方法如下所示:

void somefunction(int (*pt) [4]) ;

当且仅当pt是函数的形式参量时,也可以作如下的声明:

void somefunction(int pt[][4]) ;

注意到第一对方括号是空的。这个空的方括号表示pt是一个指针,这种变量的使用方法和junk一样。程序清单10.17中的例子就将使用上面两种声明的方法。注意清单中展示了原型语法的3种等价形式。

//array2d.c --处理二维数组的函数*/
#include <stdio.h>
#define ROWS 3
#define COLS 4
void sum_rows(int ar[][COLS],int rows);
void sum_cols(int [][COLS],int);      //可以省略名称
int sum2d(int (*ar)[COLS],int rows); //另一种语法形式
int main(void)
{
    int junk[ROWS][COLS]={
        {2,4,6,8},
        {3,5,7,9},
        {12,10,8,6}
    };

    sum_rows(junk,ROWS);
    sum_cols(junk,ROWS);
    printf("Sum of all elements = %d\n",sum2d(junk,ROWS));

    return 0;
}
void sum_rows(int ar[][COLS],int rows)
{
    int r ;
    int c ;
    int tot ;

    for (r=0;r<rows;r++)
    {
        tot=0;
        for(c=0;c<COLS;c++)
        tot+=ar[r][c];
    printf("row %d: sum = %d\n",r,tot);
    }
}
void sum_cols(int ar[][COLS],int rows)
{
    int r ;
    int c;
    int tot;

    for (c=0;c<COLS;c++)
    {
        tot=0;
        for (r=0;r<rows;r++)
            tot+=ar[r][c];
        printf("col %d: sum = %d\n",c,tot);
    }
}
int sum2d(int ar[][COLS],int rows)
{
    int r;
    int c;
    int tot=0;

    for(r=0;r<rows;r++)
    {
        for (c=0;c<COLS;c++)
            tot+=ar[r][c];
    }
    return tot;
}

这个函数可以在多种情况下工作,例如,如果把12作为行数传递给函数,则它可以处理12行4列的数组。这是因为rows是元素的数目;然而,每个元素都是一个数组,或者看作一行,rows也就可以看作是行数。

请注意ar的使用方式同mian()中junk的使用方式一样。这是因为ar和junk是同一类型,它们都是指向包含4个int值的数组的指针。

请注意下面的声明是不正确的:int sum2(int ar[][],int rows);  //错误的声明

回忆一下,编译器会把数组符号转换成指针符号。这就意味着,ar[1]会被转换成ar+1。编译器这样转换的时候,需要知道ar所指向对象的数据大小。下面的声明:

int sum2 (int ar[][4] , int rows ) ; //合法

就表示ar指向由4个Int值构成的数组,也就是16个字节长(本系统上)的对象,所以ar+1表示“在这个地址上加上16个字节大小”。如果是空括号,编译器将不能正确处理。

也可以像下面这样,在另一对方括号中填写大小,但编译器将忽略之:

int sum2 ( int ar [3][4] ,int rows ) ;  //合法,但3将被忽略

一般地,声明N维数组的指针时,除了最左边的方括号可以留空之外,其他都需要填写数值。

int sum4d (int ar [][12][20][30],int rows) ;

这是因为首方括号表示这是一个指针,而其他方括号描述的是所指向对象的数据类型。请参看下面的等效原型表示:

int sum4d ( int (*ar) [12][20][30] , int rows);  //ar是一个指针

此处ar指向一个12x20x30的int数组。

 

 

转载于:https://my.oschina.net/idreamo/blog/728114

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值