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

《C语言解惑》5.4 配合使用一维数组与指针

关灯直达底部

从变量的角度看,C语言数组和指针与基本数据类型不同,数组和指针都属于从基本数据类型构造出来的数据类型,但又是分属于不同的数据类型,从这一点看来,它们两者之间并不存在某种关系。如果换一个角度看,C语言的数组名字就是这个数组的起始地址,指针变量用来存储地址,因为从使用的角度看,它们都涉及地址,所以在使用时,它们必然有着密切的关系。其实,任何能由数组下标完成的操作,也能用指针来实现。

5.4.1 使用一维数组名简化操作

【例5.13】分别使用数组下标和数组名的例子。


#include <stdio.h>int main(){        int i, a[5],b[5];        int *p;        for(i=0;i<5;i++) {              scanf("%d",a+i);              scanf("%d",&b[i]);        }        p=a;        for(i=0;i<5;i++){             printf("%d ",*(a+i));             printf("%d ",b[i]);             printf("%d ",i[b]);               //注意这个非标准用法        }        printf("/n");        return 0;}  

因为C语言的数组名字就是这个数组的起始地址,所以两个scanf语句是等效的。数组a的各个元素地址为:a,a+1,a+2,a+3,a+4。元素值为:*a,*(a+1),*(a+2),*(a+3),*(a+4)。*a即数组a中下标为0的元素的引用,*(a+i)即数组a中下标为i的元素的引用,因此将它们简记为a[i]。显然,从书写上看,a+i和i+a的含义应该一样,因此a[i]和i[a]的含义也具有同样的含义。虽然熟悉汇编的程序员对后一种写法可能很熟悉,但不推荐在C程序中使用这种写法。这里使用这种写法,目的只是介绍一点这方面的知识。

这个程序演示了两个等效输入语句和3个等效输出语句,运行示范如下。


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

语句


printf("%d ",i[b]);  

是正确的。从运行结果可见,2[b]等价于b[2]。下面是进一步演示的例子。

【例5.14】演示数组的一种等效表示方法。


#include <stdio.h>int main(){        int a={1,2,3,4,5},i;        for(i=0; i<5; ++i)                  printf("%d ",i[a]);          //第一条输出语句        printf("/n");        for(i=0; i<5; ++i)                  printf("%p ",&i[a]);          //第二条输出语句        printf("/n");        for(i=0; i<5; ++i)                  printf("%p ",&a[i]);          //第三条输出语句        printf("/n");        return 0;}  

运行输出结果如下。


1 2 3 4 50012FF6C 0012FF70 0012FF74 0012FF78 0012FF7C0012FF6C 0012FF70 0012FF74 0012FF78 0012FF7C  

2[a]与a[2]等效,但这里i[a]也能表示i=0时的a[0],是不是很有意思?第一个输出语句输出0[a]~4[a]的值。同样,&i[a]与&a[i]的表示等效,第二行与第三行的输出验证了这一点。

注意:知道这种表示方法即可,并不推荐在编程时使用这种方法。

5.4.2 使用指针操作一维数组

【例5.15】分别使用数组名、指针、数组下标和指针下标的例子。


#include <stdio.h>int main(){       int i, a[5],b[5];       int *p;       for(i=0;i<5;i++)              scanf("%d",a+i);             scanf("%d",&b[i]);       }       p=a;       for(i=0;i<5;i++){             printf("%d ",*(a+i));             printf("%d ",b[i]);             printf("%d ",*(p+i));             printf("%d ",p[i]);       }       printf("/n");       for(i=0;i<5;i++)             scanf("%d",p+i);       for(i=0;i<5;i++)            printf("%d ",i[a]);       return 0;}  

运行示范如下:


1 2 3 4 5 6 7 8 9 101 2 1 1 3 4 3 3 5 6 5 5 7 8 7 7 9 10 9 92 4 6 8 102 4 6 8 10  

第1次输入是使数组a存入奇数,数组b存入偶数,然后用4种方法输出。第2次的输入是给数组a赋值偶数,然后输出其值。

让指针p指向数组a的地址,就可以用p[i]代替a[i]。同样,也可以使用偏移量i来表示数组各个元素的值,即*(p+i)。

由此可见,通过“p=a;”语句,就将数组和指针联系在一起了。确实,指针和数组有密切的操作关系。任何能由数组下标完成的操作(a[i]),也能用指针来实现(p[i]),而且可以使用指针自身的运算(++p或--p)简化操作。使用指向数组的指针,有助于产生占用存储空间小、运行速度快的高质量的目标代码。这也是使用数组和指针时,需要重点掌握的知识。

使指针指向数组,可以直接对数组进行操作,但要注意指针是否越界及如何处理越界。

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


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

程序运行结果如下:


1 2 3 4 51245120 5 4 3 2 1  

程序有错误。在执行


for(p=a; p<a+5;++p)  

循环结束时,执行“p=a+5”,产生越界,指向a[5]的存储首地址,*p就是a[5]的内容。但a[5]不是数组的内容,这里输出的1245120,是存储a[4]的下一个地址里的内容,不属于数组。

为了倒序输出,应该先把p的地址减1,即


for(--p;p>=a;--p)     printf("%d ",*p);  

显然,使用指针要注意的问题是移动指针出界之后,要及时将它指向正确的地方。其实,要使指针恢复到数组的首地址也很容易,只要简单地执行


p=a;  

语句即可。

另外,指针也可以像数组那样使用下标,例如:


for(i=0; i<5; i++)                    //演示指针使用下标     printf("%d ",p[i]);  

也可以使用如下方式:


for(i=0; i<5; i++)                    //演示使用指针     printf("%d ",*(p+i);  

如下程序不仅修改了原来程序的错误,还将这几种情况都同时演示一下。


#include <stdio.h>int main(){    int a={1,2,3,4,5}, *p=a, i;          //相当于int *p=&a[0];    for(i=0; i<5; ++i)               //演示3种输出方式          printf("%d %d %d %d ",a[i],*(a+i),*(p+i),p[i]);    printf("/n%u,%u/n",a,p);          //演示a即数组地址    for(i=0; i<5; i++)               //演示指针使用下标               printf("%d ",p[i]);    printf("/n");    for(; p<a+5;++p)               //演示从a[0]开始输出至a[4]          printf("%d ",*p);    printf("/n");    for(--p;p>=a;--p)               //演示从a[4]开始输出至a[0]          printf("%d ",*p);    printf("/n");    for(i=0; i<5; ++i)               //演示越界,无a[4]内容                printf("%d ",*(p+i));    printf("/n");    p=a;    for(i=0; i<5; ++i)               //正常演示,有a[4]内容          printf("%d ",*(p+i));    printf("/n");    return 0;}  

运行结果如下:


1 1 1 1 2 2 2 2 3 3 3 3 4 4 4 4 5 5 5 51245036,12450361 2 3 4 51 2 3 4 55 4 3 2 11245032 1 2 3 41 2 3 4 5  

表5-1总结了在使用时,数组和指针存在的4种对应关系。

表5-1 指针与数组的关系

【例5.17】找出下面程序的错误并改正之。


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

程序编译针对“p=&a;”给出一个警告信息,这里先不来讨论出现这种警告的原因,也不在这条语句上进行修改,而是改用标准语句以消除警告信息。

正确语句应该使用“p=a;”和“p=&a[0];”。推荐使用“p=a;”语句。修改后的程序如下。


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

程序输出结果如下:


1 2 3 4 5 5 4 3 2 1  

指向数组的指针实际上指的是能够指向数组中任一个元素的指针。这种指针应当说明为数组元素类型。这里的p指向整型数组a中的任何一个元素。使p指向a的第1个元素的最简单的方法是:


p=a;  

因为“p=&a[i]”代表下标为i的元素的地址,所以也可使用如下赋值语句指向第一个元素:


p=&a[0];  

如果要将数组单元的内容赋给指针所指向的存储单元的内容,可以使用“*”操作符,假设指针p指向数组a的首地址,则语句


*p=*a ;  

把a[0]值作为指针指向地址单元的值(等效语句*p=*a[0];)。如果p正指向数组a中的最后一个元素a[4],那么赋值语句


a[4]=789;  

也可以用语句


*p=789;  

代替。为什么一维数组与指针会存在上述操作关系呢?其实,这要追溯到数组的构成方法。数组名就是数组的首地址,指针的概念就是地址,所以说数组名就是一个指针。显然,既然a作为指针,前面的例子中的a+i和*(a+i)操作的真正含义也就很清楚了。

不过,在数组名和指针之间还是有一个重要区别的,必须记住指针是变量,故p=a或p++,p--都是有意义的操作。但数组名是指针常量,不是变量,因此表达式a=p和a++都是非法操作。但&a是存在的,为何编译系统会对“p=&a;”语句给出警告信息呢?

【例5.18】解决编译时给出的警告信息的例子。


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

针对“p=&a;”语句,编译给出如下警告信息:


warning C4047: '=' : 'int *' differs in levels of indirection from 'int (*)[5]'  

给出警告信息不影响产生执行文件,运行仍然是结果正确的。


0x0012FF6C,0x0012FF6C,0x0012FF6C,0xCCCCCCCC,0x0012FF680x0012FF6C,0x0012FF6C,0x0012FF6C,0x0012FF6C,0x0012FF680x0012FF6C,0x00000001,0x0012FF681 2 3 4 5  

由运行结果可见,系统首先给数组分配空间,而且a,&a,&a[0]都获得相同的值,这时系统为指针分配地址,但没有初始化,所以其内是无效的地址。执行


p=&a;  

时,结果正确,p也获得a的地址,输出的结果也正确,说明这条指令执行的结果,等同于用a的首地址初始化指针p。

其实,警告信息是两端数据类型不匹配造成的。p是整型指针,应该赋给它一个指针类型的地址值,所以要将&a进行类型转换,使用语句


p=(int *)&a;  

即可消除警告信息。但不主张使用这种,应使用“p=a;”。因为在运算时,数组名a是从指针形式参与运算的,“=”号两边都是指针类型。由此可见,在没有执行


p=a;  

语句之前,系统给a分配了地址(a就是数组的首地址),当然也包含a[0]和&a。所以&a跟a是等价的。假设指针现在指向a[0],则数组的第i个(下标为i)元素可表示为a[i]或*(a+i),还可使用带下标的指针p,即p[i]和*(p+i)的含义一样。若要将a的最后一个元素值设置为789,下面语句是等效的:


a[4]=789;  *(a+4)= 789;  *(p+4)= 789;  p[4]= 789;  

所以,在程序设计中,凡是用数组表示的均可使用指针来实现,一个用数组和下标实现的表达式可以等价地用指针和偏移量来实现。

注意a、&a[0]和&a的值相等的前提是在执行“p=a;”之后,请仔细分析这三者相等所代表的含义。在编程中,规范的用法是对一维数组不要使用&a,这其实是与编译系统有关的。以数组a[5]为例,C++编译系统在不同的运算场合,对a的处理方式是不一样的。对语句


sizeof (a)  

而言,输出20,代表数组a的全部长度为20个字节(每个元素4个字节,5个元素共20个字节)。语句


p=a;  

则是把a作为存储数组的首地址名处理,即“sizeof(p);”输出4,代表为指针分配4个字节。

下面再举一个错误程序,以便能正确理解指针下标的使用方法。

【例5.19】下面的程序演示了指针下标,两条输出语句等效吗?


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

有人可能会认为这两条输出语句是等效的,其实不然。下面是程序的输出结果:


5 4 3 2 14394656 1 4199289 1245120 5  

仔细分析一下,第2行的5,对应的是p[0]。其他对应p[4]~p[1]的输出都是错误的。这就是说,p[0]对应的是a[4],而p=&a[4]。也就是说,指针的下标[0],对应为指针赋值的数组内容,即p[0]=5。输出语句最后输出的是p[0],也即对应输出5。

下面的程序出现p[-1],这个下标[-1]存在吗?

【例5.20】下面的程序演示了指针下标,分析它的输出,看看是否与自己预计的一样。


#include <stdio.h>int main(){    int a={1,2,3,4,5}, *p, i;    p=a;    for(i=4; i>-1; --i)         printf("%d ",p[i]);          //第一条输出语句    printf("/n");    p=&a[2];    printf("%d %d/n",p[0],p[1]);          //第二条输出语句    p=&a[4];    printf("%d %d/n",p[0],p[-1]);          //第三条输出语句    for(i=0; i>-5; --i)         printf("%d ",p[i]);          //第四条输出语句    printf("/n");    return 0;}  

第一条输出语句很容易判别,是逆序输出5 4 3 2 1。

第二条输出语句的依据是p[0]为a[2],所以p[1]为a[3],输出为3 4。

第三条输出语句的依据是p[0]为a[4],所以p[-1]为a[3],输出为5 4。

第四条输出语句的依据是p没变,即p[0]为a[4],逆序输出5 4 3 2 1。尤其注意最后一个循环输出的顺序是p[0]、p[-1]、p[-2]、p[-3]、p[-4]。

结论:指针的下标0,是用它指向的地址作为计算依据的。

【例5.21】下面的程序演示了指针的用法,程序是否出界?


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

没有出界。程序是使用指针的偏移量,并没有移动指针。指针被设置指向a[2],所以输出是以a[2]为中心,上下移动。先输出a[3],再输出a[1],然后转去输出a[4]和a[0]。因为有一个空格,所以输出为:


3 34 25 1  

5.4.3 使用一维字符数组

一维字符数组就是字符串,它与指针的关系,不仅也具有数值数组与指针的那种关系,而且还有自己的特点。

【例5.22】改正下面程序的错误。


#include <stdio.h>int main(){      int a={1,2,3,4,5}, *p, i;    char c="abcde",*cp;    p=&a[2];    cp=&c[2];    for(i=0; i<3; ++i)    {        printf("%d%c%d%c",*(p+i),*(cp+i),*(p-i),*(cp-i));    }    printf("/n%d%s%c/n",*p,*cp,cp);    *cp='W';    cp=c; *cp='A';    printf("%c%s/n",*cp,cp);    return 0;}  

编译无错,但产生运行时错误。这是因为语句


printf("/n%d%s%c/n",*p,*cp,cp);  

有错误。*cp代表一个字符,所以要使用“%c”。而cp是存储字符串的首地址,所以将输出从cp指向的地址开始的字符串,需要用“%s”格式。将它改为


printf("/n%d%c%s/n",*p,*cp,cp);  

即可。这时cp=&c[2],*cp是c,cp开始的字符串是cde,输出应是3ccde。

修改字符串的内容只能一个一个元素地修改。将指针指向字符串的首地址既可以使用语句“cp=&c[0];”,也可以简单地使用“cp=c”。

最终的输出如下:


3c3c4d2b5e1a3ccdeAAbWde  

使用中要注意字符数组有一个结束位,所以数值数组有n个有效数组元素,而字符数组只有n-1个有效元素。因为字符数组的结束位可以作为字符数组结束的依据,所以可以将字符数组作为整体字符串输出。

5.4.4 不要忘记指针初始化

从上面的例子可见,指针很容易跑到它不该去的地方,破坏原来的内容,造成错误甚至系统崩溃。

【例5.23】下面程序从数组s中的第6个元素开始,取入10字符串存入数组t中。找出错误之处并改正之。


#include <stdio.h>int main( ){         char s[ ]="Good Afternoon!";     char t[20], p=t;     int m=6,n=10;     {       int i;       for(i=0; i<n; i++)            p[i]=s[m+i];       p[i]='/0';     }     printf(p);     printf("/n%s/n", t);     return 0;} 

要先声明指针,才能初始化。“p=t;”是错的,先声明指针*p,再使用“p=t;”。如果一次完成,应该使用“char*p=t;”。第2个语句改为:


char t[20], *p=t;  

可能有人认为“int i;”是错的。这里是在复合语句中先声明变量,后使用它,所以是对的。要注意的是第m个元素的位置不是m,应该是m-1(数组是从0开始计数)。取到n个元素,就是m-1+n个,然后再补一个结束位('/0')。这里是用i,s的下标为[m-1+i]。程序修改为如下形式:


#include <stdio.h>int main( ){     char s[ ]="Good Afternoon!";     char t[20],*p=t;     int m=6,n=10;     {       int i;       for(i=0; i<n; i++)            p[i]=s[m-1+i];       p[i]='/0';     }     printf(p);     printf("/n%s/n", t);     return 0;}  

输出结果为:


Afternoon!Afternoon!  

【例5.24】下面程序将数组t中的内容存入到动态分配的内存中。找出错误之处并改正之。


#include <stdio.h>#include <string.h>#include <stdlib.h>int main (){        int i=0;        char t="abcde";       char *p;       if ( (p=malloc ( strlen(t) ) ) == NULL )  {              printf ( "内存分配错误!/n" );              exit(1);       }       while (( p[i] = t[i]) !='/0' )            i++;        printf("%s/n",p);       return 0;}  

这个程序可以编译并正确运行,但如果从语法上讲,可以找出几个问题。首先指针初始化不对,需要强迫转换为char指针类型。另外申请的内存不够装入字符串。因为库函数strlen计算出来的是实际字符串的长度,但存入它们时,还需要增加一个标志位,即正确的形式应该为:


if ( (p=(char *)malloc ( strlen(t)+1 ) ) == NULL )  

但是,为什么能正确运行呢?这就是指针的特点了。虽然申请的内存不够,但却能正确运行。如果使用


p=(char *)malloc (1);  

语句,也能正确运行。因为毕竟给指针p分配了一个有效的地址,对指针正确地执行了初始化。至于分配的地址不够,并不限制指针的移动,这时指针可以去占用他人的地址。这一点务必引起注意,如果它跑到别人要用到的区域,就起到破坏作用,甚至造成系统崩溃。

申请内存时,要注意判别是否申请成功。在使用完动态内存之后,应该使用语句


free(p);  

释放内存,这条语句放在return语句之前即可。因为是在复制了结束位之后满足结束循环条件,所以就不能再写入结束标志了。