指针和多维数组有什么关系?为什么我们需要知道它们之间的关系?函数是通过指针来处理多维数组的,因此在使用这样的函数之前,您需要更多的了解指针。假设有如下的声明:
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的结果不同。
**对一个指针取值得到的是该指针所指向对象的数值。因为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数组。