首页 » C语言解惑 » C语言解惑全文在线阅读

《C语言解惑》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 12450362       1245040 12450403       1245044 12450444       1245048 12450484       1245052 12450521245036 12450561245120 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 55 5 4 4 3 3 2 2 1 11  

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 55 5 4 4 3 3 2 2 1 11 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