讀古今文學網 > 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語句之前即可。因為是在複製了結束位之後滿足結束循環條件,所以就不能再寫入結束標誌了。