虽然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