映月读书网 > C语言解惑 > 18.1 一维数值数组和指针 >

18.1 一维数值数组和指针

一维数组和指针具有如第一篇5.4节表5-1所示的关系。但要注意不要用错。

18.1.1 使用数组偏移量造成数组越界

【例18.1】有如下程序:


#include <stdio.h>
int main
( 
)
{
     int i
, a={1
,2
,3
,4}
,*p=a
;
     for 
( i=0
; i<5
; i++
,++p 
)
            printf
(\"%dt%ut%un\"
,*
(a+i
),a+i
,p
);
     printf
(\"%ut%u n\"
,a
,p
);
     printf
(\"%dt%dt%dt%dn\"
,*
(a+5
),a+5
,p
,*p
);
     return 0
;
}
  

编译没有出错信息。输出结果如下:


1       1245036 1245036
2       1245040 1245040
3       1245044 1245044
4       1245048 1245048
4       1245052 1245052
1245036 1245056
1245120 1245056 1245056 1245120
  

找出程序中的错误。

其实,C语言中一维数组的大小必须在编译期间确定下来。也就是说,在定义数组时,数组的大小就是一个确定的常数。即使用语句


int a={1
,2
,3
,4}
;
  

定义数组,在编译时也会将数组的大小确定下来(这里数组的大小为4),不允许再变动。

数组的下标是从0开始到4结束。所以循环语句的i应使用“i<4”,最后一个有效的数组元素是a[3],输出语句超出边界,而p则越界两个元素的存储地址。第5行的输出都是第1次越界的信息。这时,指针还要执行一次加1操作,所以它的指向是1245056,而a是数组名,所以仍然是存储数组的首地址,也就是a[0]的存储首地址1245036,这就是第6行的输出内容。

将a执行a+5,从而验证了它和p的内容一样,而*(a+5)则和*p的一致,这就是第7行的输出。

由此可见,必须知道数组的边界,如果越界,就会像指针越界一样,造成错误甚至使系统崩溃。

由以上分析知,应删除最后一个输出语句并将循环改为如下形式:


for 
( i=0
;  i<4
;  i++
, ++p 
)
  

18.1.2 使用数组名进行错误运算

【例18.2】找出下面程序的错误。


#include <stdio.h>
int main
( 
)
{
       int i
, a={1
,2
,3
,4
,5}
,*p=a
;
       p=a
;
       for 
( i=0
; i<5
; i++
,++a
,++p
)
            printf
(\"%d %d \"
,*a
,*p
);
       printf
(\"n\"
);
       p=&a[4]
;
       for 
( i=0
; i<5
; i++
,--p
)
            printf
(\"%d %d \"
,*
(a-i
),*p
);
       printf
(\"n\"
);
       a+2
;
       printf
(\"%dn\"
,*a
);
       return 0
;
}
  

错在混淆了数组和指针。对指针p来说,它可以是--p和++p。但对数组来说,a是数组名,始终代表数组存储的首地址。它虽然也相当于指针,但只是用来表示指向数组存储首地址的指针,本身不能作为左值,即“a=a+1”和“a=a-1”都是错误的。至于表达式“*(a+i)”,只是取“a[i]”数组的内容,i出现在表达式a+i中,只是表示相对a的地址偏移量,a的值并没有变化,所以是正确的。这个循环语句可以修改为:


for 
( i=0
; i<5
; i++
,++p
)
      printf
(\"%d %d \"
,*
(a+i
),*p
);
  

显然,第2个循环语句也是错的。a始终是数组名,所以a-1就越界了。从后面反序输出的起始数组是a[4],地址是&a[4],所以偏移量-i,正确的形式为:


for 
( i=0
; i<5
; i++
,--p
)
         printf
(\"%d %d \"
, *
(&a[4]-i
), *p
);
  

语句“a+2;”是无意义的,对程序运行的结果没有影响,但编译系统给出警告信息。

//改正后的完整程序


#include <stdio.h>
int main
( 
)
{
       int i
, a={1
,2
,3
,4
,5}
,*p=a
;
       p=a
;
       for 
( i=0
; i<5
; i++
,++p
)
             printf
(\"%d %d \"
,*
(a+i
),*p
);
       printf
(\"n\"
);
       p=&a[4]
;
       for 
( i=0
; i<5
; i++
,--p
)
             printf
(\"%d %d \"
,*
(&a[4]-i
),*p
);
       printf
(\"n\"
);
       printf
(\"%dn\"
,*a
);
       return 0
;
}
  

程序运行结果如下:


1 1 2 2 3 3 4 4 5 5
5 5 4 4 3 3 2 2 1 1
1
  

18.1.3 错误使用数组下标和指向数组指针的下标

【例18.3】找出下面程序的错误。


#include <stdio.h>
int main
( 
)
{
       int i
, a={1
,2
,3
,4
,5}
,*p=a
;
       p=a
;
       for 
( i=0
; i<5
; i++
)
            printf
(\"%d %d \"
, a[i]
, p[i]
);
       printf
(\"n\"
);
       p = &a[4]
;
       for 
( i=0
; i<5
; i++
)
             printf
(\"%d %d \"
, *a[4-i]
, p[4-i]
);
       printf
(\"n\"
);
       printf
(\"%d %dn\"
,*a
,*p
);
       return 0
;
}
  

第1个循环没有问题。第2个循环的关键是它们起始的下标。执行语句


p=&a[4]
;
  

之后,对指针而言,p[0]对应的是a[4],而p[1]则越界。p[-1]是a[3]。所以它的下标的计算方法是错的,应该使用p[-i]。a的表示方法与指针不一样,使用a[4-i]是正确的。计算时一定要注意,C语言的数组下标是从0开始。这里的错误是把a[4-i]误认为数组元素的指针,其实这里是标准的数组表示方法,不能使用“*”号。

执行完循环语句之后,p本身的值没有发生变化,仍然指向最后一个数组元素a[4],所以*p是最后一个数组元素的值,而a始终是数组名,*a就是第1个数组元素的值。


//
修改后的正确程序
#include <stdio.h>
int main
( 
)
{
       int i
, a={1
,2
,3
,4
,5}
,*p=a
;
       p=a
;
       for 
( i=0
; i<5
; i++
)
             printf
(\"%d %d \"
,a[i]
,p[i]
);
       printf
(\"n\"
);
       p=&a[4]
;
       for 
( i=0
; i<5
; i++
)
             printf
(\"%d %d \"
,a[4-i]
,p[-i]
);
       printf
(\"n\"
);
       printf
(\"%d %dn\"
,*a
,*p
);
       return 0
;
}
  

程序运行结果如下:


1 1 2 2 3 3 4 4 5 5
5 5 4 4 3 3 2 2 1 1
1 5
  

18.1.4 小结

从上面几个例子可以看出,使用数组和指针是相辅相成的,如果设计得好,能使程序简洁有效,达到事半功倍的效果。

1.不对称边界

C语言数组a[n]共有n个有效元素,其下标从0开始(这是有效元素的下标),至n结束,但n不是数组的有效元素,而是它不能达到的上界。有效上界是n-1。

元素个数=n-1-0+1=n

这就带来一个便利,声明数组时就给出了数组的个数,例如double b[10]就是具有10个实数的数组。而n是不可能达到的上界,区间为[0,10)。而在循环输出或赋值时,循环值小于这个n值,从而使计算简化为:

元素个数=n

对定义的数组a[n]而言,a[i](包括元素a[i])前面有i个元素,后面有n-i个元素,一共有n个元素。

2.指针的下标

a是数组的名字,也就是指向数组存储首地址的指针,a[0]是起点,a[-1]越界,下标不能为负值。

如果定义一个指向数组的指针p,则p的下标可正可负。P[0]就是初始化指针指向的数组元素,p[-1]越界。如果执行


p=&a[2]
;
  

语句,则p[0]=a[2],p[-1]=a[1],p[1]=a[3]。简言之,大于0是数组从该元素开始的正序,小于0是逆序。指针就像一朵云,可以到处飘荡,指针使用稍有不慎,就会出错。

3.灵活运用这些特征

编程中利用这些特征既可提高效率,又可避免错误。

【例18.4】有数组a[20]的值分别为:


11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
  

现在编制了如下程序,目的是把前10个的值修改为


1 2 3 4 5 6 7 8 9 10
  

并把这十个值输出以检查程序是否正确。下面的程序对吗?


#include <stdio.h>
int main 
( 
)
{
       int *p
, i
, a[20]
;
       p = a
;
       for 
(i=0
; i<20
; i++
)
             a[i]=11+i
;
       for 
(i=1
; i<=10
; i++
,*p++
)
                *p=i
;
       for 
(i=0
; i<10
; i++
, p++
)
              printf
(\"%4d\"
, *p
);
       printf
(\"n\"
);
       return 0
;
}
  

【解答】不对。对p操作会改变指向,但这是必要条件,不是充分条件。所以不要以为必须对p操作才会改变指向。指针变量的移动,使指针指向的地址也同步变化,即


for 
(i=1
; i<=10
; i++
,*p++
)
       *p=i
;
  

语句“*p++”的作用与“p++”一样,都移动了指针的指向。由此可见,在读入数据时,指针变量已经指向数组a[20]的第十一个元素的地址,即a[10]的地址。所以输出结果是


21  22  23  24  25  26  27  28  29  30
  

应先把指针的初始值回到&a[0],即把指针修改为指向a[0]。在输出之前简单地使用


p=a
;
  

语句即可实现。正确的程序在最后两句之前增加一句,即:


p=a
;
for 
(i=0
; i<10
; i++
, p++
)
     printf
(\"%4d\"
, *p
);
  

实际上,直接使用偏移量的概念编制程序,因为不移动指针指向,实现起来就非常简单。下面是完整的程序。


#include <stdio.h>
int main 
( 
)
{
      int *p
, i
, a[20]
;
      p = a
;
      for 
(i=0
; i<20
; i++
)
            a[i]=11+i
;
      for 
(i=0
; i<=10
; i++
)
              *
(p+i
)=1+i
;
      for 
(i=0
; i<10
; i++
)
             printf
(\"%4d\"
, *
(p+i
));
      printf
(\"n\"
);
      return 0
;
}
 

程序输出结果如下:


1   2   3   4   5   6   7   8   9  10