多维数组
前面讨论的都是一维数组,它和二维或多维数组是否相同?它们在某种程度上是相同的。然而,指针和数组名称之间的差异变得更为明显。考虑第5章末尾在井字程序中使用的数组。数组声明如下:
char board[3][3] = {
{'1','2','3'},
{'4','5','6'},
{'7','8','9'}
};
本节的例子将使用这个数组,探讨多维数组和指针的关系。
试试看:使用二维数组
这个例子说明了地址和数组board的关系:
/* Program 7.7 Two-Dimensional arrays and pointers */
#include <stdio.h>
int main(void)
{
char board[3][3] = {
{'1','2','3'},
{'4','5','6'},
{'7','8','9'}
};
printf("address of board : %p/n", board);
printf("address of board[0][0] : %p/n", &board[0][0]);
printf("but what is in board[0] : %p/n", board[0]);
return 0;
}
输出如下:
address of board : 0x0013ff67
address of board[0][0] : 0x0013ff67
but what is in board[0] : 0x0013ff67
代码的说明
可以看到,3个输出值都是相同的,从中可以得到什么推论?答案相当简单:声明一维数组时,[n1]放在数组名称之后,告诉编译器它是一个有n1个元素的数组。声明二维数组时,在第一维[n1]的后面放置第二维[n2],编译器就会创建一个大小为n1的数组,它的每个元素是一个大小为n2的数组。
如第5章所述,声明二维数组时,就是在创建一个数组的数组。因此,用数组名称和一个索引值访问这个二维数组时,例如board[0],就是在引用一个子数组的地址。仅使用二维数组名称,就是引用该二维数组的开始地址,它也是第一个子数组的开始地址。
总之,board、board[0]和&board[0][0] 的数值相同,但它们并不是相同的东西。
也就是说,表达式board[1]和board[1][0]的地址相同。这很容易理解,因为board[1][0]是第二个子数组board[1]的第一个元素。
但是,用指针记号获取数组中的值时,就会出问题。仍然必须使用间接运算符,但要非常小心。如果改变上面的例子,显示第一个元素的值,就知道原因了:
/* Program 7.7 A Two-Dimensional arrays */
#include <stdio.h>
int main(void)
{
char board[3][3] = {
{'1','2','3'},
{'4','5','6'},
{'7','8','9'}
};
printf("value of board[0][0] : %c/n", board[0][0]);
printf("value of *board[0] : %c/n", *board[0]);
printf("value of **board : %c/n", **board);
return 0;
}
这个程序的输出如下:
value of board[0][0] : 1
value of *board[0] : 1
value of **board : 1
可以看到,如果使用board获取第一个元素的值,就需使用两个间接运算符**board。前面的程序可以只使用一个*,是因为那是一维数组。如果只使用一个*,只会得到子数组的第一个元素,即board[0]引用的地址。
多维数组和它的子数组之间的关系如图7-3所示。
图7-3 引用数组、其子数组和元素
如图7-3所示,board引用子数组中第一个元素的地址,而board[0]、board[1]和board[2]引用对应子数组中第一个元素的地址。用两个索引值访问存储在数组元素中的值。明白了多维数组是怎么回事,下面看看如何使用board得到数组中的所有值。
试试看:得到二维数组中的所有值
这个例子用for循环进一步改进前一个例子:
/* Program 7.8 Getting the values in a two-dimensional array */
#include <stdio.h>
int main(void)
{
char board[3][3] = {
{'1','2','3'},
{'4','5','6'},
{'7','8','9'}
};
/* List all elements of the array */
for(int i = 0; i < 9; i++)
printf(" board: %c/n", *(*board + i));
return 0;
}
程序的输出如下:
board: 1
board: 2
board: 3
board: 4
board: 5
board: 6
board: 7
board: 8
board: 9
代码的说明
这个程序要注意在循环中取消引用board的方法:
printf(" board: %c/n", *(*board + i));
可以看到,使用表达式*(*board+i)可以得到一个数组元素的值。括号中的表达式*board+i会得到数组中偏移量为i的元素的地址。取消对它的引用,会得到这个地址中的值。括号在这里是很重要的。省略它们会得到board所指向的值(即存储在board中的地址所引用的值)再加上i的值。因此,如果i的值是2,*board+i会得到数组的第一个元素值加2。我们真正想要的是将i的值加到board中的地址,然后对这个新地址取消引用,得到一个值。
下面去掉例子中的括号,看看会发生什么。改变数组的初值,使字符变成从'9'到'1'。如果去掉printf()函数调用中表达式的括号:
printf(" board: %c/n", **board + i);
会得到如下输出:
board: 9
board: :
board: ;
board: <
board: =
board: >
board: ?
board: @
board: A
这是因为i的值加到数组board中的第一个元素上。在ASCII表中,得到的字符是从'9'到'A'。
另外,如果使用表达式**(board+i),一样会导致错误的结果。此时,**(board+0)指向board[0][0],而**(board+1)指向board[1][0],**(board+2)指向board[2][0]。如果增加的数值过大,就会访问数组以外的内存位置,因为这个数组没有第4个元素。
7.3.1 多维数组和指针
前面通过指针的表示法用数组名称引用二维数组,现在学习使用声明为指针的变量。如前所述,这有非常大的区别。如果声明一个指针,给它指定数组的地址,就可以用该指针访问数组的成员。
试试看:多维数组和指针
这个例子使用了多维数组和指针:
/* Program 7.9 Multidimensional arrays and pointers*/
#include <stdio.h>
int main(void)
{
char board[3][3] = {
{'1','2','3'},
{'4','5','6'},
{'7','8','9'}
};
char *pboard = *board; /* A pointer to char */
for(int i = 0; i < 9; i++)
printf(" board: %c/n", *(pboard + i));
return 0;
}
输出和前一个例子相同:
board: 1
board: 2
board: 3
board: 4
board: 5
board: 6
board: 7
board: 8
board: 9
代码的说明
这里用数组中第一个元素的地址初始化指针,然后用一般的指针算术运算遍历整个数组:
char *pboard = *board; /* A pointer to char */
for(int i = 0; i < 9; i++)
printf(" board: %c/n", *(pboard + i));
注意,取消了对board的引用(*board),得到了需要的地址,因为board是数组board[0]的地址,而不是一个元素的地址。可以用以下的方式初始化指针pboard:
char *pboard = &board[0][0];
效果相同。用下面的语句初始化指针pboard:
pboard = board; /* Wrong level of indirection! */
这是错误的。如果这么做,至少会得到编译器的警告。严格地讲,这是不合法的,因为pboard和board有不同的间接级别。这个专业术语的意思是pboard指针引用的地址包含一个char类型的值,而board引用一个地址,那个地址引用另一个含有char类型值的地址。board比pboard多了一级。因此,pboard指针需要一个*,以获得地址中的值,而board需要两个*。一些编译器允许这么用,但是会给出一条警告信息。然而,这是很糟的用法,不应这么用!
7.3.2 访问数组元素
可以使用几种方法访问二维数组的元素。表7-1列出了访问board数组的方法。最左列包含board数组的行索引值,最上面的一行包含列索引值。表中对应于给定行索引和列索引的项列出了引用该元素的各种表达式。
表7-1 访问数组元素的指针表达式
board | 0 | 1 | 2 |
0 | board[0][0] *board[0] **board | board[0][1] *(board[0]+1) *(*board+1) | board[0][2] *(board[0]+2) *(*board+2) |
1 | board[1][0] *(board[0]+3) *board[1] *(*board+3) | board[1][1] *(board[0]+4) *(board[1]+1) *(*board+4) | board[1][2] *(board[0]+5) *(board[1]+2) *(*board+5) |
2 | board[2][0] *(board[0]+6) *(board[1]+3) *board[2] *(*board+6) | board[2][1] *(board[0]+7) *(board[1]+4) *(board[2]+1) *(*board+7) | board[2][2] *(board[0]+8) *(board[1]+5) *(board[2]+2) *(*board+8) |
下面看看如何把前面所学的指针知识应用于前面没有使用指针编写的程序中,然后就可以看出基于指针的实现方式有什么不同了。第5章编写了一个计算帽子尺寸的例子,下面用另一种方式完成这个例子。
试试看:帽子尺寸的另一种计算方法
使用指针表示法重写帽子尺寸的例子:
/* Program 7.10 Understand pointers to your hat size - if you dare */
#include <stdio.h>
#include <stdbool.h>
int main(void)
{
char size[3][12] = { /* Hat sizes as characters */
{'6', '6', '6', '6', '7', '7', '7', '7', '7', '7', '7', '7'},
{'1', '5', '3', '7', ' ', '1', '1', '3', '1', '5', '3', '7'},
{'2', '8', '4', '8', ' ', '8', '4', '8', '2', '8', '4', '8'}
};
int headsize[12] = /* Values in 1/8 inches */
{164,166,169,172,175,178,181,184,188,191,194,197};
char *psize = *size;
int *pheadsize = headsize;
float cranium = 0.0; /* Head circumference in decimal inches */
int your_head = 0; /* Headsize in whole eighths */
bool hat_found = false; /* Indicates when a hat is found to fit */
bool too_small = false; /* Indicates headsize is too small */
/* Get the circumference of the head */
printf("/nEnter the circumference of your head above your eyebrows"
" in inches as a decimal value: ");
scanf(" %f", &cranium);
/* Convert to whole eighths of an inch */
your_head = (int)(8.0*cranium);
/* Search for a hat size */
for(int i = 0 ; i < 12 ; i++)
{
/* Find head size in the headsize array */
if(your_head > *(pheadsize+i))
continue;
/* If it is the first element and the head size is */
/* more than 1/8 smaller then the head is too small */
/* for a hat */
if((i == 0) && (your_head < (*pheadsize)-1))
{
printf("/nYou are the proverbial pinhead. No hat for"
"you I'm afraid./n");
too_small = true;
break; /* Exit the loop */
}
/* If head size is more than 1/8 smaller than the current */
/* element in headsize array, take the next element down */
/* as the head size */
if( your_head < *(pheadsize+i)-1)
i--;
printf("/nYour hat size is %c %c%c%c/n",
*(psize + i), /* First row of size */
*(psize + 1*12 + i), /* Second row of size */
(i==4) ?' ' : '/',
*(psize+2*12+i)); /* Third row of size */
hat_found=true;
break;
}
if(!hat_found && !too_small)
printf("/nYou, in technical parlance, are a fathead."
" No hat for you, I'm afraid./n");
return 0;
}
这个程序的输出和第5章相同,所以不再重复。这里关心的是代码本身,下面看看这个程序的新元素。
代码的说明
这个程序的执行过程和第5章相同。其区别是这个实现代码使用了指针pheadsize和psize,它们分别包含headsize数组和size数组的开始地址。your_head的值和数组的值用下面的语句作比较:
if(your_head > *(pheadsize+i))
continue;
比较运算符右侧的表达式*(pheadsize+i)等于数组表示法中的headsize[i]。括号内的表达式把i加到数组开始的地址上。给地址加一个整数值,会给该地址加上元素长度的i倍值。因此,括号内的子表达式会产生对应于索引值为i的元素的地址。然后,使用取消引用运算符*,得到这个元素的内容,将它和变量your_head的值进行比较。
如果在中间执行printf (),可以看到访问某行一个元素的指针表达式对二维数组的执行结果:
printf("/nYour hat size is %c %c%c%c/n",
*(psize + i), /* First row of size */
*(psize + 1*12 + i), /* Second row of size */
(i==4) ?' ' : '/',
*(psize+2*12+i)); /* Third row of size */
第一个表达式是*(psize+i),它访问size数组中第一行的第i个元素,等于size[0][i]。第二个表达式是*(psize + 1*12 + i),它访问size数组中第二行的第i个元素,等于size[1][i]。这个表达式说明了第二行的开始位置可以通过给psize加上行的大小来得到。接着给该结果加上i,就得到了第二行中的元素。要得到size数组中第三行的元素,可以使用表达式*(psize+2*12+i),它等于size[2][i]。