讀古今文學網 > C語言解惑 > 18.1 一維數值數組和指針 >

18.1 一維數值數組和指針

一維數組和指針具有如第一篇5.4節表5-1所示的關係。但要注意不要用錯。

18.1.1 使用數組偏移量造成數組越界

【例18.1】有如下程序:


#include <stdio.h>
int main
( 
)
{
     int i
, a={1
,2
,3
,4}
,*p=a
;
     for 
( i=0
; i<5
; i++
,++p 
)
            printf
(\"%dt%ut%un\"
,*
(a+i
),a+i
,p
);
     printf
(\"%ut%u n\"
,a
,p
);
     printf
(\"%dt%dt%dt%dn\"
,*
(a+5
),a+5
,p
,*p
);
     return 0
;
}
  

編譯沒有出錯信息。輸出結果如下:


1       1245036 1245036
2       1245040 1245040
3       1245044 1245044
4       1245048 1245048
4       1245052 1245052
1245036 1245056
1245120 1245056 1245056 1245120
  

找出程序中的錯誤。

其實,C語言中一維數組的大小必須在編譯期間確定下來。也就是說,在定義數組時,數組的大小就是一個確定的常數。即使用語句


int a={1
,2
,3
,4}
;
  

定義數組,在編譯時也會將數組的大小確定下來(這裡數組的大小為4),不允許再變動。

數組的下標是從0開始到4結束。所以循環語句的i應使用「i<4」,最後一個有效的數組元素是a[3],輸出語句超出邊界,而p則越界兩個元素的存儲地址。第5行的輸出都是第1次越界的信息。這時,指針還要執行一次加1操作,所以它的指向是1245056,而a是數組名,所以仍然是存儲數組的首地址,也就是a[0]的存儲首地址1245036,這就是第6行的輸出內容。

將a執行a+5,從而驗證了它和p的內容一樣,而*(a+5)則和*p的一致,這就是第7行的輸出。

由此可見,必須知道數組的邊界,如果越界,就會像指針越界一樣,造成錯誤甚至使系統崩潰。

由以上分析知,應刪除最後一個輸出語句並將循環改為如下形式:


for 
( i=0
;  i<4
;  i++
, ++p 
)
  

18.1.2 使用數組名進行錯誤運算

【例18.2】找出下面程序的錯誤。


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

錯在混淆了數組和指針。對指針p來說,它可以是--p和++p。但對數組來說,a是數組名,始終代表數組存儲的首地址。它雖然也相當於指針,但只是用來表示指向數組存儲首地址的指針,本身不能作為左值,即「a=a+1」和「a=a-1」都是錯誤的。至於表達式「*(a+i)」,只是取「a[i]」數組的內容,i出現在表達式a+i中,只是表示相對a的地址偏移量,a的值並沒有變化,所以是正確的。這個循環語句可以修改為:


for 
( i=0
; i<5
; i++
,++p
)
      printf
(\"%d %d \"
,*
(a+i
),*p
);
  

顯然,第2個循環語句也是錯的。a始終是數組名,所以a-1就越界了。從後面反序輸出的起始數組是a[4],地址是&a[4],所以偏移量-i,正確的形式為:


for 
( i=0
; i<5
; i++
,--p
)
         printf
(\"%d %d \"
, *
(&a[4]-i
), *p
);
  

語句「a+2;」是無意義的,對程序運行的結果沒有影響,但編譯系統給出警告信息。

//改正後的完整程序


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

程序運行結果如下:


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

18.1.3 錯誤使用數組下標和指向數組指針的下標

【例18.3】找出下面程序的錯誤。


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

第1個循環沒有問題。第2個循環的關鍵是它們起始的下標。執行語句


p=&a[4]
;
  

之後,對指針而言,p[0]對應的是a[4],而p[1]則越界。p[-1]是a[3]。所以它的下標的計算方法是錯的,應該使用p[-i]。a的表示方法與指針不一樣,使用a[4-i]是正確的。計算時一定要注意,C語言的數組下標是從0開始。這裡的錯誤是把a[4-i]誤認為數組元素的指針,其實這裡是標準的數組表示方法,不能使用「*」號。

執行完循環語句之後,p本身的值沒有發生變化,仍然指向最後一個數組元素a[4],所以*p是最後一個數組元素的值,而a始終是數組名,*a就是第1個數組元素的值。


//
修改後的正確程序
#include <stdio.h>
int main
( 
)
{
       int i
, a={1
,2
,3
,4
,5}
,*p=a
;
       p=a
;
       for 
( i=0
; i<5
; i++
)
             printf
(\"%d %d \"
,a[i]
,p[i]
);
       printf
(\"n\"
);
       p=&a[4]
;
       for 
( i=0
; i<5
; i++
)
             printf
(\"%d %d \"
,a[4-i]
,p[-i]
);
       printf
(\"n\"
);
       printf
(\"%d %dn\"
,*a
,*p
);
       return 0
;
}
  

程序運行結果如下:


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

18.1.4 小結

從上面幾個例子可以看出,使用數組和指針是相輔相成的,如果設計得好,能使程序簡潔有效,達到事半功倍的效果。

1.不對稱邊界

C語言數組a[n]共有n個有效元素,其下標從0開始(這是有效元素的下標),至n結束,但n不是數組的有效元素,而是它不能達到的上界。有效上界是n-1。

元素個數=n-1-0+1=n

這就帶來一個便利,聲明數組時就給出了數組的個數,例如double b[10]就是具有10個實數的數組。而n是不可能達到的上界,區間為[0,10)。而在循環輸出或賦值時,循環值小於這個n值,從而使計算簡化為:

元素個數=n

對定義的數組a[n]而言,a[i](包括元素a[i])前面有i個元素,後面有n-i個元素,一共有n個元素。

2.指針的下標

a是數組的名字,也就是指向數組存儲首地址的指針,a[0]是起點,a[-1]越界,下標不能為負值。

如果定義一個指向數組的指針p,則p的下標可正可負。P[0]就是初始化指針指向的數組元素,p[-1]越界。如果執行


p=&a[2]
;
  

語句,則p[0]=a[2],p[-1]=a[1],p[1]=a[3]。簡言之,大於0是數組從該元素開始的正序,小於0是逆序。指針就像一朵雲,可以到處飄蕩,指針使用稍有不慎,就會出錯。

3.靈活運用這些特徵

編程中利用這些特徵既可提高效率,又可避免錯誤。

【例18.4】有數組a[20]的值分別為:


11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
  

現在編製了如下程序,目的是把前10個的值修改為


1 2 3 4 5 6 7 8 9 10
  

並把這十個值輸出以檢查程序是否正確。下面的程序對嗎?


#include <stdio.h>
int main 
( 
)
{
       int *p
, i
, a[20]
;
       p = a
;
       for 
(i=0
; i<20
; i++
)
             a[i]=11+i
;
       for 
(i=1
; i<=10
; i++
,*p++
)
                *p=i
;
       for 
(i=0
; i<10
; i++
, p++
)
              printf
(\"%4d\"
, *p
);
       printf
(\"n\"
);
       return 0
;
}
  

【解答】不對。對p操作會改變指向,但這是必要條件,不是充分條件。所以不要以為必須對p操作才會改變指向。指針變量的移動,使指針指向的地址也同步變化,即


for 
(i=1
; i<=10
; i++
,*p++
)
       *p=i
;
  

語句「*p++」的作用與「p++」一樣,都移動了指針的指向。由此可見,在讀入數據時,指針變量已經指向數組a[20]的第十一個元素的地址,即a[10]的地址。所以輸出結果是


21  22  23  24  25  26  27  28  29  30
  

應先把指針的初始值回到&a[0],即把指針修改為指向a[0]。在輸出之前簡單地使用


p=a
;
  

語句即可實現。正確的程序在最後兩句之前增加一句,即:


p=a
;
for 
(i=0
; i<10
; i++
, p++
)
     printf
(\"%4d\"
, *p
);
  

實際上,直接使用偏移量的概念編製程序,因為不移動指針指向,實現起來就非常簡單。下面是完整的程序。


#include <stdio.h>
int main 
( 
)
{
      int *p
, i
, a[20]
;
      p = a
;
      for 
(i=0
; i<20
; i++
)
            a[i]=11+i
;
      for 
(i=0
; i<=10
; i++
)
              *
(p+i
)=1+i
;
      for 
(i=0
; i<10
; i++
)
             printf
(\"%4d\"
, *
(p+i
));
      printf
(\"n\"
);
      return 0
;
}
 

程序輸出結果如下:


1   2   3   4   5   6   7   8   9  10