10.7 指针和多维数组
int zippo[4][2]; /*内含int数组的数组*/
数组名zippo是该数组首元素的地址。在本例中,zippo的首元素是一个内含两个int值的数组,所以zip是这个内含两个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]上的值(即一个int类型的值)。与此类似,*zippo代表该数组首元素(zippo[0])的值,但是zippo[0]本身是一个int类型的地址。该值的地址是&zippo[0][0],所以*zippo就是&zippo[0][0]。对两个表达式解引用运算符表明,**zippo与*&zippo[0][0]等价,这相当于zippo[0][0],即一个int类型的值。简而言之,zippo是地址的地址,必须解引用两次才能获得原始值。地址的地址或指针的指针就是双重间接(double indirection)的例子。
显然,增加数组维数会增加指针的复杂度。
/* zippo1.c -- zippo info */
#include
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[0]和*zippo完全相同,事实上确实如此。
使用两个间接运算符(*)或者使用两对方括号([])都能获得该值(这还可以使用一个*和一个[])。
要特别注意,与zippo[2][1]等价的指针表示法是*(*(zippo+2)+1)。看上去比较复杂,应最好能理解。下面列出了理解该表达式的思路:
zippo <---二维数组首元素的地址(每个元素都是内含两个int类型元素的一维数组)
zippo+2 <---二维数组的第3个元素(即一维数组)的地址
*(zippo+2) <---二维数组的第3个元素(即一维数组)的首元素(一个int类型的值)地址
*(zippo+2)+1 <---二维数组的第3个元素(即一维数组)的第2个元素(也是一个int类型的值)地址
*(*(zippo+2)+1) <---二维数组的第3个一维数组元素的第2个int类型元素的值,即数组的第3行第2列的值(zippo[2][1])
以上分析并不是为了说明用指针表示法代替数组表示法,而是提醒读者,如果程序恰巧使用一个指向二维数组的指针,而且要通过该
指针获取值时,最好用简单的数组表示法,而不是指针表示法。
图10.5以另一种试图演示了数组地址、数组内容和指针之间的关系。
10.7.1 指向多维数组的指针
如何声明一个指针变量pz指向一个二维数组(如,zippo)?其声明如下:
int (*pz)[2]; //pz指向一个内含两个int类型值的数组
pz是一个指向一个数组的指针,该数组内含两个int类型值。
因为[]的优先级高于*,因此要在声明中使用圆括号。
int *pax[2]; //pax是一个内含两个指针元素的数组,每个元素都指向int的指针
指向二维数组的指针
/* zippo2.c -- zippo info via a pointer variable */
#include
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] == *(*(zippo + m) + n)
pz[m][n] = *(*(pz + m) + n)
10.7.2 指针的兼容性
指针之间赋值比数值类型之间的赋值要严格。不能在不同指针类型之间执行赋值操作。例如:
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; //无效,ar1是一个指向一维数组的指针
pa = ar1; //都是指向内含3个int类型元素的指针
pa = ar2; //无效,ar2是一个指向一维数组的指针,但是一维数组中致函2的int类型的元素
p2 = &pt; //都是指向int *的指针
*p2 = ar2[0]; //都是指向int的指针
p2 = ar2; //无效
一般而言,多重解引用让人费解。例如:
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指针赋给const指针
但是进行两级解引用时,这样的赋值也不安全。例如:
const int **pp2;
int *p1;
const int n = 13;
pp2 = &p1; //允许,但是这导致const限定符失效(根据第1行代码,不能通过**pp2修改它所指向的内容)
*pp2 = &n; //有效,两者都声明为const,但是这将导致p1指向n(*pp2已被修改)
*p1 = 10; //有效,但是这将改变n的值(但是根据第3行代码,不能修改n的值)
gcc和clang都给出指针类型不兼容的警告。当然,可以忽略这些警告,但是最好不要相信该程序运行的结果,这些结果都是未定义的。
C const和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]是一维数组
}
记住,如果junk是二维数组,junk[i]就是一维数组,可将其视为二维数组的一行。
然而,这种方法无法记录行和列的信息。用这种方法计算总和,行和列的信息并不重要。如果该函数要知道行和列的信息,可以通过声明正确类型的形参变量来完成,一般函数能正确地传递数组。可以这样声明函数的形参:
void somefunction( int (*pt)[4] );
另外,如果当且仅当pt是一个函数的形式参数时,可以这样声明:
void somefunction( int pt[][4] );
注意,第一个方括号是空的。空的方括号表明pt是一个指针。
演示了3种等价的原型语法
// array2d.c -- functions for 2d arrays
#include
#define ROWS 3
#define COLS 4
void sum_rows(int ar[][COLS], int rows);
void sum_cols(int [][COLS], int ); // ok to omit names
int sum2d(int (*ar)[COLS], int rows); // another syntax
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;
}
/* 输出:
*/
列数内置在函数体中,但是行数靠函数传递得到。
注意,下面的声明不正确:
int sum2( int ar[][], int rows ); //错误的声明
编译器会把数组表示法转换为指针表示法。例如,编译器会把ar[1]转换成ar+1。编译器对ar+1求值,要知道ar所指向的对象的大小。
下面的声明:
int sum2( int ar[][4], int rows ); //有效声明
表示ar指向一个内含4个int类型值的数组,所以ar+1的意思是“该地址加上16字节”。如果第2对括号是空的,编译器就不知道该怎样处理。
也可以在第1对方括号中写上大小,如下所示,但是编译器会忽略该值:
int sum2( int ar[3][4], int rows ); //有效声明,但是3被忽略
与使用typedef相比,这种形式方便得多:
typedef int arr4[4]; //arr4是一个内含4个int的数组
typedef arr4 arr3x4[3]; //arr3x4是一个内含3个arr4的数组
int sum2( arr3x4 ar, int rows ); //与下面的声明相同
int sum2( ar[3][4], int rows ); //与下面的声明相同
int sum2( ar[3][4], int rows ); //标准形式
一般而言,声明一个指向N维数组的指针时,只能省略最左边方括号中的值:
int sum4d( int ar[][12][20][30], int rows );
因为第1对方括号只用于表明这是一个指针,而其他的方括号则用于描述指针所指向数据对象的类型。下面的声明与该声明等价:
int sum4d( int (*ar)[12][20][30], int rows ); //ar是一个指针
这里,ar指向一个12*20*30的int数组。