从变量的角度看,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 5 1 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 5 0012FF6C 0012FF70 0012FF74 0012FF78 0012FF7C 0012FF6C 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 10 1 2 1 1 3 4 3 3 5 6 5 5 7 8 7 7 9 10 9 9 2 4 6 8 10 2 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 5 1245120 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 5 1245036 ,1245036 1 2 3 4 5 1 2 3 4 5 5 4 3 2 1 1245032 1 2 3 4 1 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 ,0x0012FF68 0x0012FF6C ,0x0012FF6C ,0x0012FF6C ,0x0012FF6C ,0x0012FF68 0x0012FF6C ,0x00000001 ,0x0012FF68 1 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 1 4394656 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”。
最终的输出如下:
3c3c4d2b5e1a 3ccde AAbWde
使用中要注意字符数组有一个结束位,所以数值数组有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语句之前即可。因为是在复制了结束位之后满足结束循环条件,所以就不能再写入结束标志了。