第 9章 二维数组的应用
9.1 了解二维数组
9.2 二维数组的简单应用
9.3 利用地址和指针访问二维数组
9.4 二维数组名作函数的实参
9.5 二维数组操作中的常用算法介绍
9.1 了解二维数组
9.1.1 二维数组的用途
9.1.2 二维数组的定义与一维数组一样,二维数组也必须先定义,然后才可以使用 。 定义语句的形式如下:
类型名 数组名 [常量表达式 1]
[常量表达式 2],…… ;
二维数组的定义形式与一维数组相似,
所不同的是增加了一个用方括号括起来的常量表达式。这里常量表达式 1用来指定二维数组的行数;常量表达式 2用来指定二维数组的列数。由于 C语言规定了数组下标值的下限一律为 0,所以二维数组行下标的上限为常量表达式 1的值减 1;列下标的上限为常量表达式 2的值减 1。
前面提到的存放 4名学生 3门课成绩的二维数组可定义如下:
int s[4][3];
此语句表明:这是一个名为 s的 4行 3列的二维数组;数组中一共可以存储 4× 3个整型数据;数组行下标的范围是 0~ 3,列下标的范围是 0~ 2。它在逻辑结构上可以看作以下形式的矩阵(或表格):
9.1.3 二维数组元素的引用二维数组中的元素在逻辑上可以看作构成了一个矩阵,但在物理上仍旧占据的是一串连续的存储单元 。 这些元素在内存中的排列顺序是,按行,存放,即:先放第 0行的元素,再放第 1行的元素,依次类推 。
如有定义,int x[2][3];
则数组 x在内存中的存储结构如图 9-1
所示:
图 9-1 二维数组存储结构示意图二维数组每个元素都具有一个名字 —
— 带有双下标的变量 。 它的一般表示形式为:
数组名 [下标表达式 1][下标表达式 2]
如图 9 - 1 中 所 标 出 的 x[0][0]、
x[0][1],…… 这里每一维的下标都可以是整型的常量,变量或表达式 。 如,x[i][j]、
x[0][j+1]等都是合法的 。 注意,C语言中二维数组元素的两个下标是分别放在两个方括号中的,不要误写成 x[0,0],x[i,j]这种非法形式 。
二维数组元素的引用与一维数组相同,
也能够参与同类型变量允许的所有操作。
例如:
scanf("%d",&x[0][0]);
/* 输入 */
x[1][0]=x[0][0];
/* 赋值 */
if (x[0][0]>10) …
/* 条件判断 */
而语句,x[2][3]=0;则是错误的。因为在定义语句中限定了 x数组是一个 2× 3的数组,其行下标最大值为 1,列下标最大值为
2。上述引用造成了下标越界。同样是
x[2][3],初学者要注意区分它出现在定义语句中和元素引用时的不同含义。
9.1.4 二维数组的初始化与一维数组一样,可以在定义二维数组的同时为其元素赋初值 。
( 1) 分行给二维数组的所有元素赋初值 。 例如:
int x[3][4]={ {1,2,3,4},{5,6,7,8},{9,10,11,12} };
这种形式比较规范和直观:最外层有一对花括号,在其内部,每一行元素的初值分别括在一对花括号中,中间用逗号分隔 。
( 2)分行为二维数组中部分元素提供初值。例如:
int x[3][4]={ {1},{2},{3} };
赋值后,x数组中各元素的值为:
1 0 0 0
2 0 0 0
3 0 0 0
显然,当某一行花括号内初值个数少于该行的元素个数时,系统自动补以初值 0。
又如:
int x[3][4]={ {1,2},{3,4} };
赋值后,数组元素值为:
1 2 0 0
3 4 0 0
0 0 0 0
也就是说,当行花括号少于数组的行数时,系统自动为后面的各行补以初值 0。
( 3)为二维数组赋初值时省略行花括号。例如:
int x[3][4]={1,2,3,4,5,6,7,8,9,10,11,12};
这里只保留了最外侧的花括号,而省略了用来界定行元素的花括号。此时,系统将按照数组元素在内存中的排列顺序,
依次将数据赋给各个元素。
若数据的个数少于元素的个数,系统将自动为其赋初值 0。例如:
int x[3][4]={1,2,3};
赋值后的数组元素值为:
1 2 3 0
0 0 0 0
0 0 0 0
( 4)通过赋初值确定二维数组中第一维的大小。
在定义二维数组时,可以省略第 1个方括号中的常量表达式 1,但不允许省略第
2个方括号中的常量表达式 2。例如:
int x[ ][4]={ {0,2},{3,5,6},{7} };
以上语句所赋初值中带有 3个用来界定行元素的花括号对。因此,省略的第一维的大小就是 3。它等价于:
int x[3][4]={ {0,2},{3,5,6},{7} };
也可以省略行花括号对,采用以下赋值形式:
int y[ ][4]={1,2,3,4,5,6};
由于第二维(列数)是确定的,初值个数给定了,第一维(行数)的大小就是惟一的。
计算方法是:
① 用初值个数( 6)除以第二维的大小( 4);
② 若能除尽,商就是第一维的大小;
否则商数 +1是第一维的大小。
此例中 y数组第一维的大小应是 2,以上定义语句等价于:
int y[2][4]={1,2,3,4,5,6};
9.2 二维数组的简单应用例 9.2 编程为 5× 5的二维数组的左下半三角依次赋自然数 1~ 15,其余元素为 0。
输出形式如下:
1 0 0 0 0
2 3 0 0 0
4 5 6 0 0
7 8 9 10 0
11 12 13 14 15
问题分析对二维数组元素的访问通常都是在双重循环中进行的。通过控制循环变量的初值、终值和增量的变化,从而控制数组下标的变化,以实现有规律的访问二维数组中的部分元素。如:左下角、左上角、右下角、右上角、对角线等。
源程序如下:
main( )
{ int n[5][5]={0},i,j,k=1;
for(i=0; i<5; i++)
for(j=0; j<=i; j++)
n[i][j]=k++;
for(i=0; i<5; i++)
{ for(j=0; j<5; j++)
printf("%3d",n[i][j]);
printf("\n");
}
}
程序中先通过定义时的初始化使数组
n中的内容为全 0。再通过双重循环为数组的左下半三角赋值。赋值的内容(自然数
1~ 15)由变量 k通过每次自增 1提供。需赋值的元素有这样的规律:每行需要赋值的元素个数与行数有关,第 1行 1个元素、第 2
行 2个元素,…… 所以,内循环变量 j的循环条件是 j<=i。 当 i=0时(第 1行),j的变化是 0~ 0,内循环执行 1次(给 1个元素赋值);当 i=1时(第 2行),j的变化是 0~ 1,
内循环执行 2次(给 2个元素赋值); ……
9.3 利用地址和指针访问二维数组与一维数组类似,访问二维数组中的元素,不仅可以使用带有双下标的下标变量形式,也可以通过地址和指针的形式来实现 。
9.3.1 二维数组与一维数组及指针的关系有如下定义:
int a[3][4],*p;
接触过其他高级语言的读者可能已经注意到:与其他语言不同,C语言在定义二维数组和引用二维数组元素时,都使用了两组独立的方括号。这不仅仅是表示形式上的差别,它反映出了 C语言中二维数组的特殊性质。
1.二维数组是由一维数组嵌套组成的
C语言中的二维数组可以被看作是一种特殊的一维数组,这个一维数组中的每个元素又是一个一维数组。例如,可以把上面定义的数组 a看作是一个一维数组,它由 3个特殊元素 a[0],a[1]和 a[2]组成,而这
3个元素又分别是包含有 4个整型元素的一维数组。其关系如图 9-2所示。
图 9-2 二维数组与一维数组、数组元素的关系示意图
2,二维数组名和一维数组名是基类型不同的地址常量在 8.3节中已介绍过:数组名是一个地址常量,它代表的是数组的首地址 。 那么作为二维数组名的 a和作为其元素的一维数组名 a[0],a[1],a[2]之间是个什么样的关系呢?
结合图 9-2可以看出数组名 a与 a[0]、
&a[0][0]表示的是相同的地址值,但它们在使用中是有本质上的区别的。 a是二维数组的首地址; a[0]是第 0行元素的首地址;
&a[0][0]是第 0行 0列元素的地址。
数组名 a的基类型是包含 4个整型元素的数组类型。表达式 a+1中,数值 1的单位是 4× 2=8个字节( 4个元素,每个元素 2字节),表达式 a+1代表的是与 a[1],&a[1][0]
相同的地址值。
而这里的一维数组名 a[0],a[1]和 a[2]
的基类型都是整型。表达式 a[0]+1中,数值 1的单位是 2个字节,它表示的是与
&a[0][1]相同的地址值。
综上所述可以得出:
a+0 与 a[0],&a[0][0] 等价
a+1 与 a[1],&a[1][0] 等价
a+i 与 a[i],&a[i][0] 等价而:
a[0]+0 与 &a[0][0] 等价
a[0]+1 与 &a[0][1] 等价
a[0]+j 与 &a[0][j] 等价
a[i]+j 与 &a[i][j] 等价读者应该还记得在 8.3.2节中得出的结论,a[i]等价于 *(a+i)。 这一等价的表示形式仍适用于二维数组,但在对其性质的理解上不要混同于一维数组。在一维数组中,
a[i]是一个占有物理存储单元的数组元素;
在二维数组中,a[i]仅代表一维数组名,不是存放元素值的实体。
如果以 *(a+i)替代 a[i],可以得到:
*(a+i)+j 与 &a[i][j] 等价通过以上介绍,可以归纳出表示二维数组 a[i][j]的地址的三种常见形式:
( 1) &a[i][j] 直接求元素地址
( 2) a[i]+j 利用一维数组名 a[i]
( 3) *(a+i)+j 利用二维数组名 a
用指针可以指向一维数组,也可以指向二维数组。例如:
p=&a[i][j];
p=a[i];
p=*(a+i);
以上语句都是合法的。因为指针 p的基类型为整型,与 a[i]的基类型相同。
而,p=a;则是非法的,因为它们的基类型不同。那么,什么样的指针才能存放像二维数组名这样的地址常量呢? —— 那就是行指针。
3.行指针的概念设有如下定义:
int a[3][4],(*pr)[4];
此处定义的指针变量 pr是一个行指针,
它可以指向包含 4个整型元素的一维数组。
注意:定义行指针时 *pr外侧的一对圆括号不能省略,否则下标运算符 [ ]的优先级高于 *,pr先与 [4]结合,代表的将是指针数组。
行指针 pr的基类型与数组名 a相同,
因此赋值语句,pr=a;是合法的。执行以上赋值操作后,表达式 pr+1就等价于 a+1,
它们加 1时的增量都是以一维数组的长度为单位的,这里是 4个整型元素。在使用中所不同的是,pr是指针变量,它的值可以改变; a是地址常量,它的值不容更改。例如:
语句 pr++;使 pr指向了 a数组的第二行;而语句 a++;则是非法的。
9.3.2 通过地址引用二维数组元素在上一节中,我们讨论了表示二维数组元素地址的几种常见形式,显然,通过地址就可以访问到数组元素。也就是说,
若有定义:
int a[3][4]i,j;
访问二维数组元素远不止 a[i][j]这一种形式。通过地址引用二维数组元素的常见形式有:
( 1) *(a[i]+j)
( 2) *(*(a+i)+j)
( 3) (*(a+i))[j]
为保证数组不越界,其中 i和 j的取值应满足条件,0≤i< 3,0≤j< 4。
9.3.3 通过指针数组引用二维数组元素有以下定义:
int a[3][4],*pa[3],i;
定义语句中的 pa首先与 [3]结合,表明它是一个具有 3个元素的一维数组,pa前面带有 *号,说明数组中的每个元素都是基类型为整型的指针。所以称 pa为指针数组。
可以通过以下循环使指针数组与二维数组建立联系:
for(i=0; j<3; i++)
pa[i]=a[i];
因为 a[i]与 pa[i]具有相同的基类型(整型),赋值后,指针数组 pa的元素依次指向了数组 a每行的起始地址。
其关系如图 9-3所示。
从图中可以看出,pa[0]中存放的是
a[0][0]的地址,pa[0]+1等价与 a[0]+1,增量 1代表的是一个整型单元( 2个字节),
所以表达式 pa[0]+1代表的也就是 a[0][1]的地址。
在 9.3.2节中,我们介绍了通过地址访问二维数组元素的形式。
图 9-3 指针数组与二维数组的关系那么,当指针数组 pa与二维数组 a建立了如图 9-3所示的地址关系后,同样可以用指针数组 pa来引用二维数组 a中的元素,
等价的表示形式如下:
( 1) pa[i][j] 等价于 a[i][j]
( 2) *(pa[i]+j) 等价于 *(a[i]+j)
( 3) *(*(pa+i)+j) 等价于 *(*(a+i)+j)
( 4) (*(pa+i))[j] 等价于 (*(a+i))[j]
仍要提请读者注意:引用形式的等价是建立在指针数组与二维数组上述关系的基础上的。在使用中,指针数组元素 pa[i]
可以被重新赋值从而指向其他元素;地址常量 a[i]中的值是不能改变的。这时等价关系将被破坏。
9.4 二维数组名作函数的实参二维数组元素也可以作为实参传递,
使用方法与一维数组元素作实参相似,此处不再赘述 。
二维数组名作为实参传递时,传送的虽然也是地址值,但其基类型是一个一维数组,因此对应的形参必须是行指针 。
9.5 二维数组操作中的常用算法介绍
9.5.1 查找
9.5.2 矩阵运算
9.5.3 特殊矩阵的生成