從變量的角度看,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語句之前即可。因為是在複製了結束位之後滿足結束循環條件,所以就不能再寫入結束標誌了。