从变量的角度看,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语句之前即可。因为是在复制了结束位之后满足结束循环条件,所以就不能再写入结束标志了。