虽然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 -3a[0][0]= 21 a[0][1]= 17 a[0][2]= 65a[1][0]= 17 a[1][1]= 65 a[1][2]= -96a[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.61.100000 2.200000 3.300000 4.400000 5.500000 6.600000输入数组b:1.11 2.22 3.33 4.44 5.55 6.661.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.61.100000 2.200000 3.3000004.400000 5.500000 6.6000006.600000 5.500000 4.4000003.300000 2.200000 1.100000输入数组b:1.11 2.22 3.33 4.44 5.55 6.666.660000 5.550000 4.4400003.330000 2.220000 1.1100001.110000 2.220000 3.3300004.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.61.100000 2.200000 3.3000004.400000 5.500000 6.6000006.600000 5.500000 4.4000003.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 1415 16 17 18 1920 21 22 23 2410 11 12 13 1415 16 17 18 1920 21 22 23 2410 11 12 13 1415 16 17 18 1920 21 22 23 2410 11 12 13 1415 16 17 18 1920 21 22 23 2410 15 2011 16 2112 17 2213 18 2314 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