映月读书网 > C语言解惑 > 5.4 配合使用一维数组与指针 >

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