一维数组和指针具有如第一篇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