10.7 指针和多维数组
处理多维数组的函数要用到指针,,所以在使用这种函数之前,先要深入地学习指针。
Q1:指针和多维数组有什么关系?
int zippo[4][2];
然后数组名zippo是该数组元素的地址。在本例中,zippo的首元素是一个内含两个int值的数组,所以zippo是这个内含两个int值的数组的地址。
- 因为zippo是数组元素的首元素的地址,所以zippo的值和&zippo[0]的值相同。而zippo[0]本身是一个内含两个整数的数组,所以zippo[0]的值和它首元素(一个整数)的地址(即&zippo[0][0]的值)相同。
简而言之,zippo[0]是一个占用一个int大小对象的地址。而zippo是一个占用两个int大小对象的地址。由于这个整数和内存两个整数的数组都开始于同一个地址。所以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]上的值,与此类似,*zippo代表数组元素(zippo[0])的值,但是zippo[0]本身是一个int类型值的地址。该值的地址是&zippo[0][0],所以*zippo就是&zippo[0][0].对两个表达式应用解引用运算符表明,**zippo与*&zippo[0][0]等价,这相当于zippo[0][0],即一个in类型的值。简单来说zippo是地址的地址,必须解引用两次才能获得原始值。地址的地址或指针的指针就是双重间接(double indirection)的例子
显然,增加数组维数会增加指针难度。
//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]=%p\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的地址和一维数组zippo[0]的地址相同。它们的地址都是各自数组元素首元素的地址,因而与&zippo[0][0]的值也相同。
它们也有差别,int是4个字节,zippo[0]指向一个4字节的数据对象。zippo[0]加1,其值+4.数组名zippo是一个内含2个int类型值的数组的地址,所以zippo指向一个8字节的数据对象。因此zippo+1它所指向的地址+8字节。
该程序演示了zippo[0]和*zippo完全相同,实际上确实如此。然后对二维数组名解引用两次,得到存储在数组中的值。使用两个间接运算符*或者使用两对[]都能获得该值。
与zippo[2][1]等价的指针表示法是*(*(zippo+2)+1).

10.7.1 指向多维数组的指针
如何声明一个指针变量pz指向一个二维数组?
在编写处理类似zippo这样的二维数组时会用到这样的指针。
把指针声明为指向int的类型还不够。因为指向int只能与zippo[0]的类型匹配,说明该指针指向一个int类型的值。但是zippo是它的首元素的地址,该元素是一个内含两个int类型值的一维数组。因此,pz必须指向一个内含两个int类型的数组,而不是指向一个int类型值,其声明如下:
int (* pz)[2]; //pz指向一个内含两个int类型值的数组
以上代码把pz声明为指向一个数组的指针,该数组内含连个int值。为什么使用括号,因为[]的优先级高于*.
int *pax[2]; //pax是一个内含两个指针元素的数组,每个元素都指向int的指针
由于[]的优先级高,咸鱼pax结合,所以pax成为一个内含两个元素的指针。然后*表示pax数组内含两个指针。最后int表示pax数组中的指针都指向int类型的值。因此,这行代码声明了两个指向int的指针。而前面有圆括号的版本,*先于pz结合,因此声明的是一个指向数组(内含两个int类型的值)的值。
#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是一个指针,不是数组名,但是也可以使用pz[2][1]这样的写法,可以用数组表示法或指针表示法来表示一个数组元素,即可以使用数组名,也可以使用指针名:
zippo[m][n] == *(*(pz+m)+n)
pz[m][n] == *(*(pz+m)+n)
10.7.2 指针的兼容性
指针之间的赋值比数值类型之间的赋值要严格。例如,不用类型转换就可以把int类型的值赋给double类型的变量,但是两个类型的指针不能这样做。
int n = 5;
double x;
int *p1 = &n;
double *pd = &x;
x = n; //隐形类型转换
pd = p1; //编译时错误
更复杂的类型也是如此。
int *pt;
int (*pa)[3];
int ar1[2][3];
int ar2[3][2];
int **p2; //一个指向指针的指针
有如下语句
pt = &ar1[0][0]; //都是指向int的指针
pt = ar1[0]; //都是指向int的指针
pt = ar1; //无效
pa = ar1; //都是指向内含3个int类型元素组的指针
pa = ar2; //无效
p2 = &pt; //都是int类型指针
*pt = ar2[0]; //都是指向int类型的指针
p2 = ar2; //无效
注意,以上无效的赋值表达式语句中涉及的两个指针都是指向不同的类型。例如:pt指向一个int类型值,而ar1指向一个内含3个int类型元素的数组。类似的,pa指向一个内含3个int类型元素的数组,所以它于ar1的类型兼容,但是ar2指向一个内含两个int类型元素的数组,所以pa与ar2不兼容。
变量p2是指向指针的指针,它指的指针指向int,而ar2是指向数组的指针,该数组内含两个int类型的元素。所以,p2和ar2的类型不同,不同把ar2赋给p2。但是,*p2是指向int的指针,与ar2[0]兼容。因为ar2[0]是指向该数组首元素(ar2[0][0])的指针,所以ar2[0]也是指向int的指针。
一般而言,多重解引用让人费解。例如:
int x =20;
const int y = 23;
int *p1 = &x;
const int *p2 = &y;
const int **pp2;
p1 = p2; //不安全--把const指针赋给非const指针
p2 = p1; //有效,把非const指针赋给const指针
pp2 = &p1; //不安全,嵌套指针类型赋值
前面提到过,把const指针赋给非const指针不安全,因为这样可以使用新的指针改变const指针的数据。编译器在编译代码时,可能会给处警告,执行这样的代码是未定义的。但是把非const指针赋给const指针是没问题的,前提是只进行一级接引用:
p2 = p1;
但是进行两级解引用时,这样的赋值也不安全。
const int **pp2;
int *p1;
const int n = 13;
pp2 = &p1; //有效,但是这导致const限定符失效(根据第一行,不能通过*pp2修改它所指向的内容)
*pp2 = &n; //有效两个声明都是const,但是这将导致p1指向n (*pp2已被修改)
*p1 = 10; //有效,但是这样将改变n的值。
如前所示,标准规定了通过非const指针更改const数据是未定义的。
Cconst与C++const
C和c++中的const的用法很相似,但是并不完全相同。区别之一是,C++允许在声明数组大小时使用const整数,而C却不允许。区别之二是,C++的指针检查更严格:
const int y;
const int *p2 = &y;
int *p1;
p1 = p2; //C++中不允许这样做,但是C可能只给出警告
C++不允许把const指针赋给非const指针。而C则允许这样做,但是如果通过p1更改y,其行为是未定义的。
10.7.3 函数和多维数组
如果要编写处理二维数组的函数,首先要能正确的地理解指针才能写出声明函数的形参。在函数体中,通常采用数组表示法进行相关的操作。
编写一个处理二维数组的函数。一种方法是,利用for循环把处理一维数组的函数应用到二维数组的每一行。如下:
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]是一维数组
#include<stdio.h>
int sum(int ar[],int hang);
int main(void)
{
int a[3][3]={{1,2,3},{4,5,6},{7,8,9}};
int i,j;
int total = 0;
for(i=0;i<3;i++)
total +=sum(a[i],3);
printf("%d",total);
return 0;
}
int sum(int ar[],int hang)
{
int j;
int total=0;
for(j=0;j<hang;j++)
total+=ar[j];
return total;
}
如果junk是二维数组,junk[i]就是一维数组,可将其视为一维数组的一行。这里sum()函数计算二维数组的每行总和,然后for循环把每行的总行加起来。
然而,这种方法无法记录行和列信息。用这种方法计算总和,行和列的信息并不重要。但如果每行代表一年,每列代表一月,就还需要一个函数计算某列的总和。该函数要知道行和列的信息,可以通过声明正确类型的形参变量来完成,以便函数能正确的传递数组。
数组junk是一个内含3个元素的数组,每个元素是内含4个int类型值的数组通过前面的讨论可知,这表明junk是一个指向数组的指针。可以这样声明函数的形参:
void somefunction(int (*pt)[4]);
另外,如果当且仅当pt是一个函数的形参时,咳哟这样声明:
void somefunction(int pt[][4]);
第一个方括号是空的。空的方括号表明pt是一个指针。
这样的变量稍后能以同样的方式作用于junk。
// array2d.c --处理二维数组
#include<stdio.h>
#define ROWS 3
#define COLS 4
void sum_rows(int ar[][COLS],int rows);
void sum_cols(int (*ar)[COLS],int rows);
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(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);
}
}
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;
}

程序清单中的程序把数组名junk(即指向数组首元素的指针,首元素是子数组)和符号常量ROWS(代表行数3)作为参数传递给函数。
每个函数都把ar视为内涵数组元素(每个元素都是内含4个int类型的数组)的数组。列数内置在函数体中,但是行数靠函数传递得到。如果传入函数的行数是12,那么函数要处理的是12×4的数组。因为rows 是元素个数,因为每个元素都是数组,或者视为一行,rows也可以看成是行数。
注意:ar和main()中的junk都使用数组表述法,因为ar和junk的类型相同,它们都是指向内含4个int类型值的数组的指针。
下面声明不正确
int sum2(int [][],int rows);
编译器会把数组表示法转换成指针表示法。例如,ar[1]转换成ar+1编译器对ar+1求值,要知道ar所指向的对象大小。
int sum2(int [][4],int rows);
表示ar指向一个内含4个int类型值的数组(ar指向的对象占16字节),所以ar+1的意思是该地址加上16字节。如果第二个方括号是空,编译器也不知道如何处理。
也可以在第一对方括号写上值,但是编译器会忽略该值。
int sum2(int ar[3][4],int rows); //有效声明,但是3将会被忽略
与使用typedef相比,这种形式方便得更多:
typedef int arr4[4]; //arr4是一个内含4个int 的数组
typedef arr4 arr3×4[3]; //arr3×4是一个内含3个arr4的数组
int sum2(arr3×4 ar,int rows); //与下相同
int sum2(int ar[3][4],int rows);
int sum2(int ar[][4],int rows); //标准形式
一般而言,声明一个指向N维数组的指针时,只能省略最左边方括号中的值:
int sum4d(int ar[][12][20][20][30],int rows);
因为第一队方括号只用于表明这是一个指针,其他的方括号则用于描述指针所指向数据对象的类型。
下面声明与之等价:
int sum4d(int (*ar)[12][20][30],int rows);
这里ar指向一个12×20×30的int数组。
叨叨几句... NOTHING