对于二维数组而言,因为计算机存储器是一维的,所以二维数组实质上就是一种抽象数组。也就是说,编译程序必须实现从所使用的抽象数组到组成计算机存储的实际的一维数组的映射。以二维数组为例,关键也是确定该数组的大小和获得指向该数组下标为0的元素的指针。
5.5.1 数组操作及越界和初始化错误
【例5.25】下面的程序编译没有警告信息,但运行输出“2 0 0 15”之后出错。找出下面程序中的错误。
#include <stdio.h> int main ( ) { int i ,a[3][3]={1 ,2 ,3} ; a[3][1]=15 ; for ( i=0 ; i<=3 ; i++ ) printf (\"%d \" ,a[i][1] ); return 0 ; }
因为C语言不检查数组越界,所以程序编译正确,运行时出错。原因是程序在初始化数组a时,只给第1行赋值,其他元素均为0值。二维数组a[m][n]的边界是m行和n列,起始点a[0][0]。a[3][1]处于第3行,所以越界。数组从0行0列开始,没有第3行和第3列的元素。
for循环“i=3”越界,应该使用“i<3”。如果没有这个循环语句,上一条语句就会出错。有了这条语句,则在输出a[3][1]后出错。
需要注意的是,a[3][3]是二维数组,不要误以为“3”代表三维数组。记住这里的二维是指平面,三维是指立体空间,必须具有三个维数指标才是三维数组,例如a[1][2][2],虽然下标都小于3,但却是三维数组。
【例5.26】为二维数组赋值和输出的典型程序。
#include <stdio.h> int main ( ) { int a[3][3] ,i ,j ; for ( i=0 ; i<3 ; i++ ) for ( j=0 ; j<3 ; j++ ) a[i][j]=i+j ; for ( i=0 ; i<3 ; i++ ) { for ( j=0 ; j<3 ; j++ ) { printf (\"%d \" ,a[i][j] ); } printf (\"n\" ); } return 0 ; }
程序运行结果如下:
0 1 2 1 2 3 2 3 4
这是使用两个循环语句处理二维数组的典型方法,注意处理的循环次序,就可以使用如下方法以加深理解。对第1行而言,可以用a[0]作为a[0][0],随j的变化,地址a[0]+j依次遍历a[0][1],a[0][2]。第2行用a[1]作为首地址,地址a[1]+j依次遍历a[1][1],a[1][2]。下面的程序演示了三种方法。
【例5.27】比较分别用3种方法访问数组的演示程序。
#include <stdio.h> int main ( ) { int a[3][3] ,i ,j ; // 分别给每行元素赋值 for (i=0 , j=0 ; j<3 ; j++ ) // 访问a[0] 所在行 * (a[0]+j )=i+j ; for (i=1 , j=0 ; j<3 ; j++ ) // 访问a[1] 所在行 * (a[1]+j )=i+j ; for ( i=2 ,j=0 ; j<3 ; j++ ) // 访问a[2] 所在行 * (a[2]+j )=i+j ; // 标准输出方法 for ( i=0 ; i<3 ; i++ ){ for ( j=0 ; j<3 ; j++ ){ printf (\"%d \" ,a[i][j] ); } printf (\"n\" ); } // 以a[0] 为基准分别输出每行元素 for (j=0 ; j<3 ; j++ ) // 输出第1 行 printf (\"%d \" , * (a[0]+j )); printf (\"n\" ); for (j=0 ; j<3 ; j++ ) // 输出第2 行 printf (\"%d \" , * (a[0]+3+j )); //a[1]=a[0]+3 printf (\"n\" ); for (j=0 ; j<3 ; j++ ) // 输出第3 行 printf (\"%d \" , * (a[0]+6+j )); //a[2]=a[0]+6 printf (\"n\" ); // 使用计算公式表示地址的输出方法 for ( i=0 ; i<3 ; i++ ){ for ( j=0 ; j<3 ; j++ ){ printf (\"%d \" ,* (a[0]+ i*3+j )); } printf (\"n\" ); } return 0 ; }
程序运行结果如下:
0 1 2 1 2 3 2 3 4 0 1 2 1 2 3 2 3 4 0 1 2 1 2 3 2 3 4
三种输出结果相同,证明赋值使用的地址方法与其等效。现说明如下:
(1)在赋值时,使用每行元素的首地址a[0],a[1],a[2]做基准,使用列作为偏移量,计算每个元素的地址。使用三个独立的for语句分别为各行的元素赋值。
(2)使用标准的二层for循环语句输出,同时检验输入结果及其后的输出方法的等效性。
(3)a[0]的下一行是a[1],其关系是a[1]=a[0]+3。同理,a[2]=a[1]+3=a[0]+6。用a[0]作为基准,使用三个独立的for语句分别输出数组各行的内容,结果相同。
(4)根据(3),可以推出表示数组元素的一般公式为a[0]+i*3+j。使用二层for语句输出数组元素的值,结果正确。
【例5.28】下面的程序编译有警告信息,找出存在问题的语句。
#include <stdio.h> int main ( ) { int a[3][3] ,i ,*p ; p=a ; for ( i=0 ; i<9 ; i++ ) * (p+i )=i+1 ; for ( i=0 ; i<9 ; i++ ,++p ){ if (i%3==0 ) printf (\"n\" ); printf ( \"%d \" ,*p ); } printf (\"n\" ); return 0 ; }
问题出在语句“p=a;”上。数组名已经被一维数组使用,虽然这个程序运行结果正确,但已经规定a代表一维数组名,所以编译系统给出警告信息。可以使用
p= (int * )a ;
消除警告信息,但不推荐这样做;也可以改用显式的表示“&a[0][0]”。一般推荐直接使用“p=a[0]”。
这个程序是把二维数组作为连续存放的一维数组进行处理,把它们看做从首地址开始的连续存储区,也就是9个元素值。程序改错后,输出结果如下:
1 2 3 4 5 6 7 8 9
为了说明警告信息和推荐用法,请看下面的例子。
【例5.29】演示二维数组首地址的例子。
#include <stdio.h> int main ( ) { int a[3][3] ; a[0][0]=9 ; printf ( \"%p %p %p %p %dn\" ,a ,&a[0][0] ,&a[0] ,a[0] ,*a[0] ); return 0 ; }
输出结果如下:
0012FF5C 0012FF5C 0012FF5C 0012FF5C 9
从运行结果可见,a,&a[0][0],&a[0],a[0]的值相等,都是首地址值。但编译系统必须能分辨一维数组和二维数组。所以对二维数组而言,a[0]就被作为二维数组存储的首地址。显式的表示是“&a[0][0]”。“p=a;”是一维数组的首地址表示方法,如果用在二维数组中,编译系统就会给出警告信息。使用“p=&a[0][0]”也可以,但更推荐直接使用“p=a[0]”。
注意*a[0]值的对应关系,这就是后面要用到的方法。
5.5.2 二维数组与指针
【例5.30】下面的程序是为数组赋值并按3行输出数组内容。找出存在的错误,并使用二重for循环和指针下标改写程序。
#include <stdio.h> int main ( ) { int a[3][3] ,i ,*p ; for (i=0 , p=a[0] ; i<9 ; p++ ,i++ ) *p=i+1 ; for (p ,i=0 ; i<9 ; i++ ,--p ) { printf ( \"%d \" ,*p ); if (i%3==0 ) printf (\"n\" ); } printf (\"n\" ); return 0 ; }
因为程序使用移动指针的方法赋值,在最后一次满足赋值条件之后,指针指向数组之外,所以在逆序输出时,要调整指针的指向,让它指向最后一个元素,即需要做减1操作。
还有一个错误是语句
if (i%3==0 ) printf (\"n\" );
的位置不对,输出第1个即满足判断条件,产生换行,使输出结果为4行。可将它提前到printf语句之前,这时会先产生一个空行。可以这条语句修改为
if (i !=0&&i%3==0 ) printf (\"n\" );
的形式,实现3行输出。完整的程序为:
#include <stdio.h> int main ( ) { int a[3][3] ,i ,*p ; for (i=0 , p=a[0] ; i<9 ; p++ ,i++ ) *p=i+1 ; for (--p ,i=0 ; i<9 ; i++ ,--p ) { if (i !=0&&i%3==0 ) printf (\"n\" ); printf ( \"%d \" ,*p ); } printf (\"n\" ); return 0 ; }
程序输出为:
9 8 7 6 5 4 3 2 1
因为程序中移动指针指向的地址,所以产生越界。如果使用偏移量的方式,就不会移动指针指向的位置,也就不会发生这种问题。
下面使用二重for循环和指针下标改写程序,这时要注意使用指针和数组之间的变化关系,正确计算指针的下标。其实,例5.27已经给出了推算方法。这里是让指针p指向数组元素a[0]的地址。在指针变量指向数组首地址之后,引用该数组第i行第j列元素的方法如下:
* (指针变量 + i * 列数 +j )
使用scanf赋值时,需要使用地址。相应地址的表示方法如下:
( 指针变量 + i * 列数 +j )
如果指针变量处于数组最后,i和j仍按升序,引用该数组第i行第j列元素的方法如下:
* (指针变量 -i * 列数 -j )
使用scanf赋值时,需要使用地址。相应地址的表示方法如下:
( 指针变量 - i * 列数 -j )
【例5.31】使用二重for循环和指针下标的程序。
#include <stdio.h> int main ( ) { int a[3][3] ,i ,j ,*p ; p=a[0] ; for ( i=0 ; i<3 ; i++ ) for ( j=0 ; j<3 ; j++ ) * (p+i*3+j )=i*3+j+1 ; // 等效p[i*3+j]=i*3+j+1 ; for ( i=0 ; i<3 ; i++ ) { for ( j=0 ; j<3 ; j++ ) printf ( \"%d \" ,p[i*3+j] ); // 等效printf ( \"%d \" ,* (p+i*3+j )); printf ( \"n\" ); } p=&a[2][2] ; printf (\"%dn\" ,*p ); for ( i=0 ; i<3 ; i++ ) { for (j=0 ; j<3 ; j++ ) printf ( \"%d \" ,p[-i*3-j] ); // 等效printf ( \"%d \" ,* (p-i*3-j )); printf ( \"n\" ); } }
运行结果如下。
1 2 3 4 5 6 7 8 9 9 9 8 7 6 5 4 3 2 1
运行结果验证了如上论述。下面的程序演示了指针的使用方法。程序中分别使用指针输出第1个元素和最后一个元素的值,分别正序和逆序写入及输出数组的内容。可以对照输出结果,仔细体会指针和数组的关系。
【例5.32】使用二重for循环和指针下标的程序。
#include <stdio.h> int main ( ) { int a[3][3] ,i ,j ,*p ; p=a[0] ; for ( i=0 ; i<3 ; i++ ) for ( j=0 ; j<3 ; j++ ) p[i*3+j]=i*3+j+1 ; for ( i=0 ; i<3 ; i++ ) { for ( j=0 ; j<3 ; j++ ) printf ( \"%d \" ,* (p+i*3+j )); printf ( \"n\" ); } printf (\"n\" ); printf ( \"%d \" ,p[0] ); p=&a[2][2] ; printf ( \"%dn\" ,p[0] ); for ( i=0 ; i<3 ; i++ ) { for ( j=0 ; j<3 ; j++ ) printf ( \"%d \" ,* (p-i*3-j )); printf ( \"n\" ); } // 使用的是偏移量,*p 仍然是最后一个元素 printf ( \"%dn\" ,*p ); // 使用最后一个元素的地址重新赋值 for ( i=0 ; i<3 ; i++ ) for ( j=0 ; j<3 ; j++ ) p[-i*3-j]=99-i*3-j ; // 等效 * (p-i*3-j )=99-i*3-j ; // 重新赋值 for ( i=0 ; i<3 ; i++ ) { for ( j=0 ; j<3 ; j++ ) printf ( \"%d \" ,* (p-i*3-j )); // 等效printf ( \"%d \" ,p[-i*3-j] ); printf ( \"n\" ); } printf (\"n\" ); // 逆向输出 for ( i=0 ; i<3 ; i++ ) { for ( j=0 ; j<3 ; j++ ,--p ) printf ( \"%d \" ,*p ); printf ( \"n\" ); } ++p ; // 越界,调整指向第1 个元素的地址 printf ( \"%dn\" ,*p ); // 输出第1 个元素 return 0 ; }
程序输出结果如下:
1 2 3 4 5 6 7 8 9 1 9 9 8 7 6 5 4 3 2 1 9 99 98 97 96 95 94 93 92 91 99 98 97 96 95 94 93 92 91 91
5.5.3 二维数组与指向一维数组的指针
【例5.33】本例使用指向某个一维数组的指针变量对二维数组进行操作的例子。分析一下是否存在错误?
#include <stdio.h> int main ( ) { int i , j , a[3][3] ,(*p )[3] ; p=a ; for ( i=0 ; i<3 ; i++ ) for (j=0 ; j<3 ; j++ ) * (* (p+i )+j )=i*3+j+1 ; for ( i=0 ; i<3 ; i++ ) for (j=0 ; j<3 ; j++ ){ if (i !=0&&j%3==0 ) printf (\"n\" ); printf ( \"%d \" ,* (* (p+i )+j )); } printf (\"n\" ); printf (\"%d ,%d ,%dn\" ,*p[0] ,* (p[0]+1 ),* (p[0]+2 )); // 输出第1 行 printf (\"%d ,%d ,%dn\" ,*p[0] ,*p[1] ,*p[2] ); // 输出第1 列 printf (\"%d ,%d ,%dn\" ,**p ,* (*p+1 ),* (*p+2 )); // 输出第1 行 printf (\"%d ,%d ,%dn\" ,**p ,** (p+1 ),** (p+2 )); // 输出第1 列 printf (\"%d ,%d ,%dn\" ,* (* (p+1 )),* (* (p+1 )+1 ),* (* (p+1 )+2 )); // 输出第2 行 printf (\"%d ,%d ,%dn\" ,* (*p+1 ),* (* (p+1 )+1 ),* (* (p+2 )+1 )); // 输出第2 列 return 0 ; }
程序中使用
p=a ;
是正确的,不会给出警告信息。这是因为语句
int (*p )[3] ;
定义的指针变量p,是一个指向一维数组a的指针变量,“=”号两边数据类型相同。反之,如果这时使用a[0],却要给出警告信息。除了可以使用语句
p=a ;
初始化指针之外,也可在声明时使用
int (*p )[3]=a ;
语句直接进行初始化。
引用数组元素和对应地址的方法如下:
* (p + i ) +j // 数组元素的对应地址 * ( * (p + i ) +j ) // 数组元素
这个程序还演示了输出一行、一列、二行和二列的方法,用来加深对引用数组元素和对应地址的理解。程序运行结果如下。
1 2 3 4 5 6 7 8 9 1 ,2 ,3 1 ,4 ,7 1 ,2 ,3 1 ,4 ,7 4 ,5 ,6 2 ,5 ,8