虽然C语言只有一维数组,但它的数组元素可以是任何类型的对象,这也包括是另外一个数组。因此,通过这个特性可以很容易“仿真”出一个多维数组。C语言一般很少使用多于三维的数组,最常用的是一维和两维数组。所以这里仅以二维数组为例。
18.4.1 二维数组的界限
假设二维数值数组a[m][n],其起点是a[0][0],上边界是a[m][n]。越界就是m行n列。假设有指针p,指向首地址是“p=a[0]”或“p=&a[0][0]”,特别要预防这个设置错误。
【例18.9】先输出二维数组中的全部元素,再输出为负值的元素,最后按序号输出全部元素。找出程序中的错误。
#include <stdio.h> int main ( ) { int a[3][3]={21 ,17 ,65 ,-96 ,-58 ,31 ,-99 ,-3 ,8} ; int i ,j ,k=0 ,*p=a[0] ; for ( i=0 ; i<9 ; i++ ) { if (i !=0&&i%3==0 ) printf (\"n\" ); printf (\"%4d\" ,p[i] ); } printf (\"n\" ); for ( i=0 ; i<9 ; i++ ) { if (i !=0&&i%3==0 ) printf (\"n\" ); if (* (p+i )<0 ) printf (\"%4d\" ,* (p+i )); } printf (\"n\" ); for ( i=0 ; i<3 ; i++ ){ for ( j=0 ; j<3 ; j++ ) printf ( \"a[%d][%d]=%4d \" ,i ,j ,* (p+i+j ) ); printf (\"n\" ); } return 0 ; }
可以像一维数组那样使用指针的下标和偏移量,这时只需要一个for循环。这种方法的缺点是没有数组的标识符号。如果要使用数组标识,则需要使用双重for循环,这时使用
printf ( \"a[%d][%d]=%4d \" , i , j , a[i][j] );
语句输出数组元素的值最直观方便。
程序如果使用指针配合双重循环语句输出,则要换算偏移量。这个程序存在标号计算错误。i=1时,偏移量的计算与标号不对应,从如下程序的输出可以看出它的问题。
21 17 65 -96 -58 31 -99 -3 8 -96 -58 -99 -3 a[0][0]= 21 a[0][1]= 17 a[0][2]= 65 a[1][0]= 17 a[1][1]= 65 a[1][2]= -96 a[2][0]= 65 a[2][1]= -96 a[2][2]= -58
在第2行,应该从3+j开始,第3行应从6+j开始。计算公式为3*i+j。这条语句修改为
printf ( \"a[%d][%d]=%4d \" ,i ,j ,* (p+i*3+j ) );
即可。
如果使用指针读入数据,也要注意换算。使用二重for循环,在指针变量指向数组首地址之后,引用该数组第i行第j列的元素的方法如下:
*(指针变量+i*列数+j)
使用scanf赋值时,需要使用地址。相应地址的表示方法如下:
(指针变量+i*列数+j)
在指针变量指向数组尾地址之后,引用该数组第i行第j列的元素的方法如下:
*(指针变量-i*列数-j)
使用scanf赋值时,需要使用地址。相应地址的表示方法如下:
(指针变量-i*列数-j)
【例18.10】下面的程序对数组a采用正序读入和输出,对数组b采用反序读入和输出,找出程序中的错误。
#include <stdio.h> int main ( ) { int i ,j ; double a[2][3] ,b[2][3] ,*p=a[0] ; printf (\" 输入数组a :\" ); for ( i=0 ; i<2 ; i++ ) for ( j=0 ; j<3 ; j++ ) scanf (\"%lf\" ,(p+i*3+j ) ); for ( i=0 ; i<2 ; i++ ) for ( j=0 ; j<3 ; j++ ) printf (\"%lf \" ,* (p+i*3+j ) ); printf (\"n\" ); printf (\" 输入数组b :\" ); p=&b[2][3] ; for ( i=0 ; i<2 ; i++ ) for ( j=0 ; j<3 ; j++ ) scanf (\"%lf\" ,(p-i*3-j ) ); for ( i=0 ; i<2 ; i++ ) for ( j=0 ; j<3 ; j++ ) printf (\"%lf \" ,* (p-i*3-j ) ); printf (\"n\" ); return 0 ; }
数组b的最后一个元素是b[1][2],不是b[2][3]。将指针初始化改为
p=&b[1][2] ;
即可。程序运行示范如下:
输入数组a : 1.1 2.2 3.3 4.4 5.5 6.6 1.100000 2.200000 3.300000 4.400000 5.500000 6.600000 输入数组b : 1.11 2.22 3.33 4.44 5.55 6.66 1.110000 2.220000 3.330000 4.440000 5.550000 6.660000
结论:m行n列的二维数组是从0行0列到m-1行n-1列。元素个数是m×n个,其界限是处于m行和n列上的位置。
这与数学上的行列式定义不一样,要特别注意,以免越界。
18.4.2 二维数组的一维特性
因为二维数组是在一维数组的基础上构造的,所以下标是连续的,可以直接使用一维数组的方式读入和输出数据。关键问题与一维数组一样,就是不要混淆0号单元的标识。
【例18.11】编写程序使用两种方法为二维数组中的部分元素赋值。
假设实数数组a[2][3]和b[2][3],对a数组使用指针偏移量读入数据,然后正序和反序输出其内容。因为没有移动指针,所以指针的下标是正数。对b数组使用指针偏移量读入数据,然后将指针调整到p[0]处,使用负下标正序和反序输出其内容。
// 完整的程序 #include <stdio.h> int main ( ) { int i ; double a[2][3] ,b[2][3] ,*p=a[0] ; printf (\" 输入数组a :\" ); for ( i=0 ; i<6 ; i++ ) scanf (\"%lf\" ,p+i ); for ( i=0 ; i<6 ; i++ ) { if (i !=0&&i%3==0 ) printf (\"n\" ); printf (\"%lf \" ,p[i] ); } printf (\"n\" ); for ( i=5 ; i>-1 ; i-- ) { printf (\"%lf \" ,p[i] ); if (i%3==0 ) printf (\"n\" ); } printf (\"n\" ); printf (\" 输入数组b :\" ); p=b[0] ; for ( i=0 ; i<6 ; i++ ,p++ ) scanf (\"%lf\" ,p ); for ( --p ,i=0 ; i>-6 ; i-- ) { if (i !=0&&i%3==0 ) printf (\"n\" ); printf (\"%lf \" ,p[i] ); } printf (\"n\" ); for ( i=-5 ; i<1 ; i++ ) { printf (\"%lf \" ,p[i] ); if (i%3==0 ) printf (\"n\" ); } printf (\"n\" ); return 0 ; }
程序运行示范如下:
输入数组a : 1.1 2.2 3.3 4.4 5.5 6.6 1.100000 2.200000 3.300000 4.400000 5.500000 6.600000 6.600000 5.500000 4.400000 3.300000 2.200000 1.100000 输入数组b : 1.11 2.22 3.33 4.44 5.55 6.66 6.660000 5.550000 4.440000 3.330000 2.220000 1.110000 1.110000 2.220000 3.330000 4.440000 5.550000 6.660000
与一维数组一样,千万不能混淆p[0]。
【例18.12】使用一维数组的读写方法,演示二维数组的赋值和输出。
这个程序不使用双重循环,直接使用一维数组的方式读入和输出数据。只要注意到这时a数组的存储首地址是a[0],就可以很容易写出它们的程序。
#include <stdio.h> int main ( ) { int i ; double a[2][3] ; printf (\" 输入数组a :\" ); for ( i=0 ; i<6 ; i++ ) scanf (\"%lf\" ,a[0]+i ); for ( i=0 ; i<6 ; i++ ) { if (i !=0&&i%3==0 ) printf (\"n\" ); printf (\"%lf \" ,* (a[0]+i )); } printf (\"n\" ); for ( i=5 ; i>-1 ; i-- ) { printf (\"%lf \" ,* (a[0]+i )); if (i%3==0 ) printf (\"n\" ); } printf (\"n\" ); return 0 ; }
程序示范运行如下:
输入数组a : 1.1 2.2 3.3 4.4 5.5 6.6 1.100000 2.200000 3.300000 4.400000 5.500000 6.600000 6.600000 5.500000 4.400000 3.300000 2.200000 1.100000
由此可见,如果不需要输出数组下标,直接使用一维数组的形式进行操作,反而简单。
18.4.3 指向二维数组的指针
上面都是使用普通的指针指向数组,所以产生连续的标识运算。通过下面的例子可以比较几种方法的优缺点。
【例18.13】引入指向二维数组的一维指针概念。
对于二维数组a[3][5],固定首行地址,移动列序号得到如下对应关系:
a[0]+j j=0,1,2,3,4 *(a[0]+j)遍历第0行,i=0,j=0~4
a[1]+j j=0,1,2,3,4 *(a[1]+j)遍历第1行,i=1,j=0~4
a[2]+j j=0,1,2,3,4 *(a[2]+j)遍历第2行,i=2,j=0~4
显然,这些表达式比用*(a[0]+i*5+j)的含义明确。
如果将a[i]使用一维指针p[i]表示,显然有:
p[0]+j j=0,1,2,3,4 *(p[0]+j)遍历第0行,i=0,j=0~4
p[1]+j j=0,1,2,3,4 *(p[1]+j)遍历第1行,i=1,j=0~4
p[2]+j j=0,1,2,3,4 *(p[2]+j)遍历第2行,i=2,j=0~4
当j固定,则按列输出。以第1列为例,则有
a[i]+1 i=0,1,2 *(a[i]+1)遍历第1列,i=0~2
p[i]+1 i=0,1,2 *(p[i]+1)遍历第1列,i=0~2
按此方法,读者可以自行给出其他4列的表示方法。
如果使用i行和j列表示,则有:*(*(p+i)+j)。显然前者含义较准确。
假设语句
int *p ;
声明的是整型指针变量。一维数组使用
p=a ;
的格式。而
p=a[0] ;
是二维数组首地址。指向二维数组的一维数组指针的格式与二维数组的列数有关。假设二维数组的列数为m,应声明为
int (*p )[m] ;
指向二维数组首地址的格式与一维数组的一样,即
p=a ;
下面的程序比较几种输出方式,编程时可以根据实际情况灵活选择。
#include <stdio.h> int main ( ) { int i ,j ; int a[3][5] ; int (*p )[5] ; // 声明一维指针 for ( i=0 ; i<15 ; i++ ) * (a[0]+i )=i+10 ; // 直接使用数组首地址 // 注意不要错为a for ( i=0 ; i<3 ; i++ ){ for ( j=0 ; j<5 ; j++ ) printf (\"%d \" ,* (a[0]+i*5+j ) ); // 换算 printf (\"n\" ); } printf (\"n\" ); for ( i=0 ; i<3 ; i++ ){ for ( j=0 ; j<5 ; j++ ) printf (\"%d \" ,* (a[i]+j ) ); // 使用a[i] 形式 printf (\"n\" ); } printf (\"n\" ); p=a ; // 指向二维数组首地址 //p 指向a 的第一个元素,也就是数组a 的3 个有着5 个元素的 // 数组类型元素之一 for ( i=0 ; i<3 ; i++ ){ for ( j=0 ; j<5 ; j++ ) printf (\"%d \" ,* (p[i]+j ) ); // 指针下标 printf (\"n\" ); } printf (\"n\" ); for ( i=0 ; i<3 ; i++ ){ for ( j=0 ; j<5 ; j++ ) printf (\"%d \" ,* (* (p+i )+j ) ); // 换算 printf (\"n\" ); } // 按列输出 for ( j=0 ; j<5 ; j++ ){ for ( i=0 ; i<3 ; i++ ) printf (\"%d \" ,* (p[i]+j ) ); // 指针下标 printf (\"n\" ); } printf (\"n\" ); return 0 ; }
程序运行输出结果如下:
10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 10 15 20 11 16 21 12 17 22 13 18 23 14 19 24
对二维字符串来说,专业的使用方法就是直接使用列,所以与定义一个一维字符指针的用法一样。区别是字符串数组能将一行字符串作为整体输出。
【例18.14】找出下面程序中的错误。
#include <stdio.h> #include <string.h> int main ( ) { int i ,a[5]={85 ,80 ,88 ,98 ,80} ; char c[5][5] ; char *p ; // 赋值 strcpy (c[0] ,\" 数学\" ); strcpy (c[1] ,\" 物理\" ); strcpy (c[2] ,\" 外语\" ); strcpy (c[3] ,\" 政治\" ); strcpy (c[4] ,\" 体育\" ); p=c[0] ; for (i=0 ;i<5 ;i++ ) printf (\"%s :%dn\" ,p[i] ,* (a+i )); return 0 ; }
从二维数值数组的使用方法来看。这里好像没有错误。其实仔细想一想就会发现问题。字符指针加1,是移动存储一个字符的位置。这里每个字符串是4位,连结束符在内,共占5个字节,所以指针要移动5个位置,才能到第2个字符串。将循环语句改为
for (i=0 ; i<5 ; i++ , p=p+5 ) printf (\"%s :%dn\" , p , * (a+i ));
一般的二维字符数组的字符串长度并不相等,本程序的方法也就失效了。由此可见,使用字符变量指针不适合二维字符串数组。
其实,声明
char c[5][5] ;
就隐含一维字符串指针
char (* )[5] ;
下面使用一维字符指针编制这个程序。程序中也给出使用数组名指针的方法,以便对照理解。
#include <stdio.h> #include <string.h> int main ( ) { int i ,a[5]={85 ,80 ,88 ,98 ,80} ; char c[5][5] ; char (*p )[5] ; // 赋值 strcpy (c[0] ,\" 数学\" ); strcpy (c[1] ,\" 物理\" ); strcpy (c[2] ,\" 外语\" ); strcpy (c[3] ,\" 政治\" ); strcpy (c[4] ,\" 体育\" ); for (i=0 ;i<5 ;i++ ) // 使用数组名c 的下标 printf (\"%s :%dn\" ,c[i] ,* (a+i )); printf (\"n\" ); p=c ; for (i=0 ;i<5 ;i++ ) // 使用一维字符指针的下标 printf (\"%s :%dn\" ,p[i] ,* (a+i )); return 0 ; }
运行结果如下:
数学:85 物理:80 外语:88 政治:98 体育:80 数学:85 物理:80 外语:88 政治:98 体育:80