映月读书网 > C语言解惑 > 18.4 二维数组和指针 >

18.4 二维数组和指针

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