第七章 指標

pamxy發表於2013-07-01

轉自:http://www.72up.com/c/chapter_7.htm

指標簡介

  指標是C語言中廣泛使用的一種資料型別。 運用指標程式設計是C語言最主要的風格之一。利用指標變數可以表示各種資料結構; 能很方便地使用陣列和字串; 並能象組合語言一樣處理記憶體地址,從而編出精練而高效的程式。指標極大地豐富了C語言的功能。 學習指標是學習C語言中最重要的一環, 能否正確理解和使用指標是我們是否掌握C語言的一個標誌。同時, 指標也是C語言中最為困難的一部分,在學習中除了要正確理解基本概念,還必須要多程式設計,上機除錯。只要作到這些,指標也是不難掌握的。

  指標的基本概念 在計算機中,所有的資料都是存放在儲存器中的。 一般把儲存器中的一個位元組稱為一個記憶體單元, 不同的資料型別所佔用的記憶體單元數不等,如整型量佔2個單元,字元量佔1個單元等, 在第二章中已有詳細的介紹。為了正確地訪問這些記憶體單元, 必須為每個記憶體單元編上號。 根據一個記憶體單元的編號即可準確地找到該記憶體單元。記憶體單元的編號也叫做地址。 既然根據記憶體單元的編號或地址就可以找到所需的記憶體單元,所以通常也把這個地址稱為指標。 記憶體單元的指標和記憶體單元的內容是兩個不同的概念。 可以用一個通俗的例子來說明它們之間的關係。我們到銀行去存取款時, 銀行工作人員將根據我們的帳號去找我們的存款單, 找到之後在存單上寫入存款、取款的金額。在這裡,帳號就是存單的指標, 存款數是存單的內容。對於一個記憶體單元來說,單元的地址即為指標, 其中存放的資料才是該單元的內容。在C語言中, 允許用一個變數來存放指標,這種變數稱為指標變數。因此, 一個指標變數的值就是某個記憶體單元的地址或稱為某記憶體單元的指標。圖中,設有字元變數C,其內容為“K”(ASCII碼為十進位制數 75),C佔用了011A號單元(地址用十六進數表示)。設有指標變數P,內容為011A, 這種情況我們稱為P指向變數C,或說P是指向變數C的指標。 嚴格地說,一個指標是一個地址, 是一個常量。而一個指標變數卻可以被賦予不同的指標值,是變。 但在常把指標變數簡稱為指標。為了避免混淆,我們中約定:“指標”是指地址, 是常量,“指標變數”是指取值為地址的變數。 定義指標的目的是為了通過指標去訪問記憶體單元。
  
   既然指標變數的值是一個地址, 那麼這個地址不僅可以是變數的地址, 也可以是其它資料結構的地址。在一個指標變數中存放一
個陣列或一個函式的首地址有何意義呢? 因為陣列或函式都是連續存放的。通過訪問指標變數取得了陣列或函式的首地址, 也就找到了該陣列或函式。這樣一來, 凡是出現陣列,函式的地方都可以用一個指標變數來表示, 只要該指標變數中賦予陣列或函式的首地址即可。這樣做, 將會使程式的概念十分清楚,程式本身也精練,高效。在C語言中, 一種資料型別或資料結構往往都佔有一組連續的記憶體單元。 用“地址”這個概念並不能很好地描述一種資料型別或資料結構, 而“指標”雖然實際上也是一個地址,但它卻是一個資料結構的首地址, 它是“指向”一個資料結構的,因而概念更為清楚,表示更為明確。 這也是引入“指標”概念的一個重要原因。

指標變數的型別說明

  對指標變數的型別說明包括三個內容:
(1)指標型別說明,即定義變數為一個指標變數;
(2)指標變數名;
(3)變數值(指標)所指向的變數的資料型別。
   其一般形式為: 型別說明符 *變數名;
   其中,*表示這是一個指標變數,變數名即為定義的指標變數名,型別說明符表示本指標變數所指向的變數的資料型別。
   例如: int *p1;表示p1是一個指標變數,它的值是某個整型變數的地址。 或者說p1指向一個整型變數。至於p1究竟指向哪一個整型變數, 應由向p1賦予的地址來決定。
   再如:
staic int *p2; /*p2是指向靜態整型變數的指標變數*/
float *p3; /*p3是指向浮點變數的指標變數*/
char *p4; /*p4是指向字元變數的指標變數*/ 應該注意的是,一個指標變數只能指向同型別的變數,如P3 只能指向浮點變數,不能時而指向一個浮點變數, 時而又指向一個字元變數。

指標變數的賦值

  指標變數同普通變數一樣,使用之前不僅要定義說明, 而且必須賦予具體的值。未經賦值的指標變數不能使用, 否則將造成系統混亂,甚至當機。指標變數的賦值只能賦予地址, 決不能賦予任何其它資料,否則將引起錯誤。在C語言中, 變數的地址是由編譯系統分配的,對使用者完全透明,使用者不知道變數的具體地址。 C語言中提供了地址運算子&來表示變數的地址。其一般形式為: & 變數名; 如&a變示變數a的地址,&b表示變數b的地址。 變數本身必須預先說明。設有指向整型變數的指標變數p,如要把整型變數a 的地址賦予p可以有以下兩種方式:
(1)指標變數初始化的方法 int a;
int *p=&a;
(2)賦值語句的方法 int a;
int *p;
p=&a;
不允許把一個數賦予指標變數,故下面的賦值是錯誤的: int *p;p=1000; 被賦值的指標變數前不能再加“*”說明符,如寫為*p=&a 也是錯誤的

指標變數的運算

  指標變數可以進行某些運算,但其運算的種類是有限的。 它只能進行賦值運算和部分算術運算及關係運算。
1.指標運算子

(1)取地址運算子&
   取地址運算子&是單目運算子,其結合性為自右至左,其功能是取變數的地址。在scanf函式及前面介紹指標變數賦值中,我們已經瞭解並使用了&運算子。

(2)取內容運算子*
   取內容運算子*是單目運算子,其結合性為自右至左,用來表示指標變數所指的變數。在*運算子之後跟的變數必須是指標變數。需要注意的是指標運算子*和指標變數說明中的指標說明符* 不是一回事。在指標變數說明中,“*”是型別說明符,表示其後的變數是指標型別。而表示式中出現的“*”則是一個運算子用以表示指標變數所指的變數。
main(){
int a=5,*p=&a;
printf ("%d",*p);
}
......
表示指標變數p取得了整型變數a的地址。本語句表示輸出變數a的值。

2.指標變數的運算

(1)賦值運算

指標變數的賦值運算有以下幾種形式:
①指標變數初始化賦值,前面已作介紹。

②把一個變數的地址賦予指向相同資料型別的指標變數。例如:
int a,*pa;
pa=&a; /*把整型變數a的地址賦予整型指標變數pa*/

③把一個指標變數的值賦予指向相同型別變數的另一個指標變數。如:
int a,*pa=&a,*pb;
pb=pa; /*把a的地址賦予指標變數pb*/
由於pa,pb均為指向整型變數的指標變數,因此可以相互賦值。 ④把陣列的首地址賦予指向陣列的指標變數。
例如: int a[5],*pa;
pa=a; (陣列名錶示陣列的首地址,故可賦予指向陣列的指標變數pa)
也可寫為:
pa=&a[0]; /*陣列第一個元素的地址也是整個陣列的首地址,
也可賦予pa*/
當然也可採取初始化賦值的方法:
int a[5],*pa=a;

⑤把字串的首地址賦予指向字元型別的指標變數。例如: char *pc;pc="c language";或用初始化賦值的方法寫為: char *pc="C Language"; 這裡應說明的是並不是把整個字串裝入指標變數, 而是把存放該字串的字元陣列的首地址裝入指標變數。 在後面還將詳細介紹。

⑥把函式的入口地址賦予指向函式的指標變數。例如: int (*pf)();pf=f; /*f為函式名*/

(2)加減算術運算

  對於指向陣列的指標變數,可以加上或減去一個整數n。設pa是指向陣列a的指標變數,則pa+n,pa-n,pa++,++pa,pa--,--pa 運算都是合法的。指標變數加或減一個整數n的意義是把指標指向的當前位置(指向某陣列元素)向前或向後移動n個位置。應該注意,陣列指標變數向前或向後移動一個位置和地址加1或減1 在概念上是不同的。因為陣列可以有不同的型別, 各種型別的陣列元素所佔的位元組長度是不同的。如指標變數加1,即向後移動1 個位置表示指標變數指向下一個資料元素的首地址。而不是在原地址基礎上加1。
例如:
int a[5],*pa;
pa=a; /*pa指向陣列a,也是指向a[0]*/
pa=pa+2; /*pa指向a[2],即pa的值為&pa[2]*/ 指標變數的加減運算只能對陣列指標變數進行, 對指向其它型別變數的指標變數作加減運算是毫無意義的。(3)兩個指標變數之間的運算只有指向同一陣列的兩個指標變數之間才能進行運算, 否則運算毫無意義。

①兩指標變數相減
兩指標變數相減所得之差是兩個指標所指陣列元素之間相差的元素個數。實際上是兩個指標值(地址) 相減之差再除以該陣列元素的長度(位元組數)。例如pf1和pf2 是指向同一浮點陣列的兩個指標變數,設pf1的值為2010H,pf2的值為2000H,而浮點陣列每個元素佔4個位元組,所以pf1-pf2的結果為(2000H-2010H)/4=4,表示pf1和 pf2之間相差4個元素。兩個指標變數不能進行加法運算。 例如, pf1+pf2是什麼意思呢?毫無實際意義。

②兩指標變數進行關係運算
指向同一陣列的兩指標變數進行關係運算可表示它們所指陣列元素之間的關係。例如:
pf1==pf2表示pf1和pf2指向同一陣列元素
pf1>pf2表示pf1處於高地址位置
pf1<pf2表示pf2處於低地址位置
main(){
int a=10,b=20,s,t,*pa,*pb;
pa=&a;
pb=&b;
s=*pa+*pb;
t=*pa**pb;
printf("a=%d\nb=%d\na+b=%d\na*b=%d\n",a,b,a+b,a*b);
printf("s=%d\nt=%d\n",s,t);
}
......
說明pa,pb為整型指標變數
給指標變數pa賦值,pa指向變數a。
給指標變數pb賦值,pb指向變數b。
本行的意義是求a+b之和,(*pa就是a,*pb就是b)。
本行是求a*b之積。
輸出結果。
輸出結果。
......
指標變數還可以與0比較。設p為指標變數,則p==0表明p是空指標,它不指向任何變數;p!=0表示p不是空指標。空指標是由對指標變數賦予0值而得到的。例如: #define NULL 0 int *p=NULL; 對指標變數賦0值和不賦值是不同的。指標變數未賦值時,可以是任意值,是不能使用的。否則將造成意外錯誤。而指標變數賦0值後,則可以使用,只是它不指向具體的變數而已。
main(){
int a,b,c,*pmax,*pmin;
printf("input three numbers:\n");
scanf("%d%d%d",&a,&b,&c);
if(a>b){
pmax=&a;
pmin=&b;}
else{
pmax=&b;
pmin=&a;}
if(c>*pmax) pmax=&c;
if(c<*pmin) pmin=&c;
printf("max=%d\nmin=%d\n",*pmax,*pmin);
}
......
pmax,pmin為整型指標變數。
輸入提示。
輸入三個數字。
如果第一個數字大於第二個數字...
指標變數賦值
指標變數賦值

指標變數賦值
指標變數賦值
判斷並賦值
判斷並賦值
輸出結果
......

陣列指標變數的說明和使用

  指向陣列的指標變數稱為陣列指標變數。 在討論陣列指標變數的說明和使用之前,我們先明確幾個關係。
一個陣列是由連續的一塊記憶體單元組成的。 陣列名就是這塊連續記憶體單元的首地址。一個陣列也是由各個陣列元素(下標變數) 組成的。每個陣列元素按其型別不同佔有幾個連續的記憶體單元。 一個陣列元素的首地址也是指它所佔有的幾個記憶體單元的首地址。 一個指標變數既可以指向一個陣列,也可以指向一個陣列元素, 可把陣列名或第一個元素的地址賦予它。如要使指標變數指向第i號元素可以把i元素的首地址賦予它或把陣列名加i賦予它。

  設有實陣列a,指向a的指標變數為pa,從圖6.3中我們可以看出有以下關係:
pa,a,&a[0]均指向同一單元,它們是陣列a的首地址,也是0 號元素a[0]的首地址。pa+1,a+1,&a[1]均指向1號元素a[1]。類推可知a+i,a+i,&a[i]
指向i號元素a[i]。應該說明的是pa是變數,而a,&a[i]都是常量。在程式設計時應予以注意。
main(){
int a[5],i;
for(i=0;i<5;i++){
a[i]=i;
printf("a[%d]=%d\n",i,a[i]);
}
printf("\n");
}
主函式
定義一個整型陣列和一個整型變數
迴圈語句
給陣列賦值
列印每一個陣列的值
......
輸出換行
......
陣列指標變數說明的一般形式為:
型別說明符 * 指標變數名
   其中型別說明符表示所指陣列的型別。 從一般形式可以看出指向陣列的指標變數和指向普通變數的指標變數的說明是相同的。
引入指標變數後,就可以用兩種方法來訪問陣列元素了。
   第一種方法為下標法,即用a[i]形式訪問陣列元素。 在第四章中介紹陣列時都是採用這種方法。
   第二種方法為指標法,即採用*(pa+i)形式,用間接訪問的方法來訪問陣列元素。
main(){
int a[5],i,*pa;
pa=a;
for(i=0;i<5;i++){
*pa=i;
pa++;
}
pa=a;
for(i=0;i<5;i++){
printf("a[%d]=%d\n",i,*pa);
pa++;
}
}
主函式
定義整型陣列和指標
將指標pa指向陣列a
迴圈
將變數i的值賦給由指標pa指向的a[]的陣列單元
將指標pa指向a[]的下一個單元
......
指標pa重新取得陣列a的首地址
迴圈
用陣列方式輸出陣列a中的所有元素
將指標pa指向a[]的下一個單元
......
......
下面,另舉一例,該例與上例本意相同,但是實現方式不同。
main(){
int a[5],i,*pa=a;
for(i=0;i<5;){
*pa=i;
printf("a[%d]=%d\n",i++,*pa++);
}
}
主函式
定義整型陣列和指標,並使指標指向陣列a
迴圈
將變數i的值賦給由指標pa指向的a[]的陣列單元
用指標輸出陣列a中的所有元素,同時指標pa指向a[]的下一個單元
......
......

陣列名和陣列指標變數作函式引數

  在第五章中曾經介紹過用陣列名作函式的實參和形參的問題。在學習指標變數之後就更容易理解這個問題了。 陣列名就是陣列的首地址,實參向形參傳送陣列名實際上就是傳送陣列的地址, 形參得到該地址後也指向同一陣列。 這就好象同一件物品有兩個彼此不同的名稱一樣。同樣,指標變數的值也是地址, 陣列指標變數的值即為陣列的首地址,當然也可作為函式的引數使用。
float aver(float *pa);
main(){
float sco[5],av,*sp;
int i;
sp=sco;
printf("\ninput 5 scores:\n");
for(i=0;i<5;i++) scanf("%f",&sco[i]);
av=aver(sp);
printf("average score is %5.2f",av);
}
float aver(float *pa)
{
int i;
float av,s=0;
for(i=0;i<5;i++) s=s+*pa++;
av=s/5;
return av;
}

指向多維陣列的指標變數

本小節以二維陣列為例介紹多維陣列的指標變數。

一、多維陣列地址的表示方法
設有整型二維陣列a[3][4]如下:
0 1 2 3
4 5 6 7
8 9 10 11
   設陣列a的首地址為1000,各下標變數的首地址及其值如圖所示。在第四章中介紹過, C語言允許把一個二維陣列分解為多個一維陣列來處理。因此陣列a可分解為三個一維陣列,即a[0],a[1],a[2]。每一個一維陣列又含有四個元素。例如a[0]陣列,含有a[0][0],a[0][1],a[0][2],a[0][3]四個元素。 陣列及陣列元素的地址表示如下:a是二維陣列名,也是二維陣列0行的首地址,等於1000。a[0]是第一個一維陣列的陣列名和首地址,因此也為1000。*(a+0)或*a是與a[0]等效的, 它表示一維陣列a[0]0 號元素的首地址。 也為1000。&a[0][0]是二維陣列a的0行0列元素首地址,同樣是1000。因此,a,a[0],*(a+0),*a?amp;a[0][0]是相等的。同理,a+1是二維陣列1行的首地址,等於1008。a[1]是第二個一維陣列的陣列名和首地址,因此也為1008。 &a[1][0]是二維陣列a的1行0列元素地址,也是1008。因此a+1,a[1],*(a+1),&a[1][0]是等同的。 由此可得出:a+i,a[i],*(a+i),&a[i][0]是等同的。 此外,&a[i]和a[i]也是等同的。因為在二維陣列中不能把&a[i]理解為元素a[i]的地址,不存在元素a[i]。

  C語言規定,它是一種地址計算方法,表示陣列a第i行首地址。由此,我們得出:a[i],&a[i],*(a+i)和a+i也都是等同的。另外,a[0]也
可以看成是a[0]+0是一維陣列a[0]的0號元素的首地址, 而a[0]+1則是a[0]的1號元素首地址,由此可得出a[i]+j則是一維陣列a[i]的j號元素首地址,它等於&a[i][j]。由a[i]=*(a+i)得a[i]+j=*(a+i)+j,由於*(a+i)+j是二維陣列a的i行j列元素的首地址。該元素的值等於*(*(a+i)+j)。
[Explain]#define PF "%d,%d,%d,%d,%d,\n"
main(){
static int a[3][4]={0,1,2,3,4,5,6,7,8,9,10,11};
printf(PF,a,*a,a[0],&a[0],&a[0][0]);
printf(PF,a+1,*(a+1),a[1],&a[1],&a[1][0]);
printf(PF,a+2,*(a+2),a[2],&a[2],&a[2][0]);
printf("%d,%d\n",a[1]+1,*(a+1)+1);
printf("%d,%d\n",*(a[1]+1),*(*(a+1)+1));
}

二、多維陣列的指標變數

  把二維陣列a 分解為一維陣列a[0],a[1],a[2]之後,設p為指向二維陣列的指標變數。可定義為: int (*p)[4] 它表示p是一個指標變數,它指向二維陣列a 或指向第一個一維陣列a[0],其值等於a,a[0],或&a[0][0]等。而p+i則指向一維陣列a[i]。從前面的分析可得出*(p+i)+j是二維陣列i行j 列的元素的地址,而*(*(p+i)+j)則是i行j列元素的值。

  二維陣列指標變數說明的一般形式為: 型別說明符 (*指標變數名)[長度] 其中“型別說明符”為所指陣列的資料型別。“*”表示其後的變數是指標型別。 “長度”表示二維陣列分解為多個一維陣列時, 一維陣列的長度,也就是二維陣列的列數。應注意“(*指標變數名)”兩邊的括號不可少,如缺少括號則表示是指標陣列(本章後面介紹),意義就完全不同了。
[Explain]main(){
static int a[3][4]={0,1,2,3,4,5,6,7,8,9,10,11};
int(*p)[4];
int i,j;
p=a;
for(i=0;i<3;i++)
for(j=0;j<4;j++) printf("%2d ",*(*(p+i)+j));
}
'Expain字串指標變數的說明和使用字串指標變數的定義說明與指向字元變數的指標變數說明是相同的。只能按對指標變數的賦值不同來區別。 對指向字元變數的指標變數應賦予該字元變數的地址。如: char c,*p=&c;表示p是一個指向字元變數c的指標變數。而: char *s="C Language";則表示s是一個指向字串的指標變數。把字串的首地址賦予s。
請看下面一例。
main(){
char *ps;
ps="C Language";
printf("%s",ps);
}
執行結果為:
C Language
上例中,首先定義ps是一個字元指標變數, 然後把字串的首地址賦予ps(應寫出整個字串,以便編譯系統把該串裝入連續的一塊記憶體單元),並把首地址送入ps。程式中的: char *ps;ps="C Language";等效於: char *ps="C Language";輸出字串中n個字元後的所有字元。
main(){
char *ps="this is a book";
int n=10;
ps=ps+n;
printf("%s\n",ps);
}
執行結果為:
book 在程式中對ps初始化時,即把字串首地址賦予ps,當ps= ps+10之後,ps指向字元“b”,因此輸出為"book"。
main(){
char st[20],*ps;
int i;
printf("input a string:\n");
ps=st;
scanf("%s",ps);
for(i=0;ps[i]!='\0';i++)
if(ps[i]=='k'){
printf("there is a 'k' in the string\n");
break;
}
if(ps[i]=='\0') printf("There is no 'k' in the string\n");
}
   本例是在輸入的字串中查詢有無‘k’字元。 下面這個例子是將指標變數指向一個格式字串,用在printf函式中,用於輸出二維陣列的各種地址表示的值。但在printf語句中用指標變數PF代替了格式串。 這也是程式中常用的方法。
main(){
static int a[3][4]={0,1,2,3,4,5,6,7,8,9,10,11};
char *PF;
PF="%d,%d,%d,%d,%d\n";
printf(PF,a,*a,a[0],&a[0],&a[0][0]);
printf(PF,a+1,*(a+1),a[1],&a[1],&a[1][0]);
printf(PF,a+2,*(a+2),a[2],&a[2],&a[2][0]);
printf("%d,%d\n",a[1]+1,*(a+1)+1);
printf("%d,%d\n",*(a[1]+1),*(*(a+1)+1));
}
   在下例是講解,把字串指標作為函式引數的使用。要求把一個字串的內容複製到另一個字串中,並且不能使用strcpy函式。函式cprstr的形參為兩個字元指標變數。pss指向源字串,pds指向目標字串。表示式:
(*pds=*pss)!=`\0'
cpystr(char *pss,char *pds){
while((*pds=*pss)!='\0'){
pds++;
pss++; }
}
main(){
char *pa="CHINA",b[10],*pb;
pb=b;
cpystr(pa,pb);
printf("string a=%s\nstring b=%s\n",pa,pb);
}
   在上例中,程式完成了兩項工作:一是把pss指向的源字元複製到pds所指向的目標字元中,二是判斷所複製的字元是否為`\0',若是則表明源字串結束,不再迴圈。否則,pds和pss都加1,指向下一字元。在主函式中,以指標變數pa,pb為實參,分別取得確定值後呼叫cprstr函式。由於採用的指標變數pa和pss,pb和pds均指向同一字串,因此在主函式和cprstr函式中均可使用這些字串。也可以把cprstr函式簡化為以下形式:
cprstr(char *pss,char*pds)
{while ((*pds++=*pss++)!=`\0');}
   即把指標的移動和賦值合併在一個語句中。 進一步分析還可發現`\0'的ASCⅡ碼為0,對於while語句只看表示式的值為非0就迴圈,為0則結束迴圈,因此也可省去“!=`\0'”這一判斷部分,而寫為以下形式:
cprstr (char *pss,char *pds)
{while (*pdss++=*pss++);}
表示式的意義可解釋為,源字元向目標字元賦值, 移動指標,若所賦值為非0則迴圈,否則結束迴圈。這樣使程式更加簡潔。簡化後的程式如下所示。
cpystr(char *pss,char *pds){
while(*pds++=*pss++);
}
main(){
char *pa="CHINA",b[10],*pb;
pb=b;
cpystr(pa,pb);
printf("string a=%s\nstring b=%s\n",pa,pb);
}

使用字串指標變數與字元陣列的區別

用字元陣列和字元指標變數都可實現字串的儲存和運算。 但是兩者是有區別的。在使用時應注意以下幾個問題:

1. 字串指標變數本身是一個變數,用於存放字串的首地址。而字串本身是存放在以該首地址為首的一塊連續的記憶體空間中並以‘\0’作為串的結束。字元陣列是由於若干個陣列元素組成的,它可用來存放整個字串。

2. 對字元陣列作初始化賦值,必須採用外部型別或靜態型別,如: static char st[]={“C Language”};而對字串指標變數則無此限制,如: char *ps="C Language";

3. 對字串指標方式 char *ps="C Language";可以寫為: char *ps; ps="C Language";而對陣列方式:
static char st[]={"C Language"};
不能寫為:
char st[20];st={"C Language"};
而只能對字元陣列的各元素逐個賦值。

  從以上幾點可以看出字串指標變數與字元陣列在使用時的區別,同時也可看出使用指標變數更加方便。前面說過,當一個指標變數在未取得確定地址前使用是危險的,容易引起錯誤。但是對指標變數直接賦值是可以的。因為C系統對指標變數賦值時要給以確定的地址。因此,
char *ps="C Langage";
或者 char *ps;
ps="C Language";都是合法的。

函式指標變數

  在C語言中規定,一個函式總是佔用一段連續的記憶體區, 而函式名就是該函式所佔記憶體區的首地址。 我們可以把函式的這個首地址(或稱入口地址)賦予一個指標變數, 使該指標變數指向該函式。然後通過指標變數就可以找到並呼叫這個函式。 我們把這種指向函式的指標變數稱為“函式指標變數”。
函式指標變數定義的一般形式為:
型別說明符 (*指標變數名)();
其中“型別說明符”表示被指函式的返回值的型別。“(* 指標變數名)”表示“*”後面的變數是定義的指標變數。 最後的空括號表示指標變數所指的是一個函式。
例如: int (*pf)();
表示pf是一個指向函式入口的指標變數,該函式的返回值(函式值)是整型。
下面通過例子來說明用指標形式實現對函式呼叫的方法。
int max(int a,int b){
if(a>b)return a;
else return b;
}
main(){
int max(int a,int b);
int(*pmax)();
int x,y,z;
pmax=max;
printf("input two numbers:\n");
scanf("%d%d",&x,&y);
z=(*pmax)(x,y);
printf("maxmum=%d",z);
}
   從上述程式可以看出用,函式指標變數形式呼叫函式的步驟如下:1. 先定義函式指標變數,如後一程式中第9行 int (*pmax)();定義pmax為函式指標變數。

2. 把被調函式的入口地址(函式名)賦予該函式指標變數,如程式中第11行 pmax=max;

3. 用函式指標變數形式呼叫函式,如程式第14行 z=(*pmax)(x,y); 呼叫函式的一般形式為: (*指標變數名) (實參表)使用函式指標變數還應注意以下兩點:

a. 函式指標變數不能進行算術運算,這是與陣列指標變數不同的。陣列指標變數加減一個整數可使指標移動指向後面或前面的陣列元素,而函式指標的移動是毫無意義的。

b. 函式呼叫中"(*指標變數名)"的兩邊的括號不可少,其中的*不應該理解為求值運算,在此處它只是一種表示符號。

指標型函式

前面我們介紹過,所謂函式型別是指函式返回值的型別。 在C語言中允許一個函式的返回值是一個指標(即地址), 這種返回指標值的函式稱為指標型函式。
定義指標型函式的一般形式為:
型別說明符 *函式名(形參表)
{
…… /*函式體*/
}
其中函式名之前加了“*”號表明這是一個指標型函式,即返回值是一個指標。型別說明符表示了返回的指標值所指向的資料型別。
如:
int *ap(int x,int y)
{
...... /*函式體*/
}
   表示ap是一個返回指標值的指標型函式, 它返回的指標指向一個整型變數。下例中定義了一個指標型函式 day_name,它的返回值指向一個字串。該函式中定義了一個靜態指標陣列name。name 陣列初始化賦值為八個字串,分別表示各個星期名及出錯提示。形參n表示與星期名所對應的整數。在主函式中, 把輸入的整數i作為實參, 在printf語句中呼叫day_name函式並把i值傳送給形參 n。day_name函式中的return語句包含一個條件表示式, n 值若大於7或小於1則把name[0] 指標返回主函式輸出出錯提示字串“Illegal day”。否則返回主函式輸出對應的星期名。主函式中的第7行是個條件語句,其語義是,如輸入為負數(i<0)則中止程式執行退出程式。exit是一個庫函式,exit(1)表示發生錯誤後退出程式, exit(0)表示正常退出。

  應該特別注意的是函式指標變數和指標型函式這兩者在寫法和意義上的區別。如int(*p)()和int *p()是兩個完全不同的量。int(*p)()是一個變數說明,說明p 是一個指向函式入口的指標變數,該函式的返回值是整型量,(*p)的兩邊的括號不能少。int *p() 則不是變數說明而是函式說明,說明p是一個指標型函式,其返回值是一個指向整型量的指標,*p兩邊沒有括號。作為函式說明, 在括號內最好寫入形式引數,這樣便於與變數說明區別。 對於指標型函式定義,int *p()只是函式頭部分,一般還應該有函式體部分。
main(){
int i;
char *day_name(int n);
printf("input Day No:\n");
scanf("%d",&i);
if(i<0) exit(1);
printf("Day No:%2d-->%s\n",i,day_name(i));
}
char *day_name(int n){
static char *name[]={ "Illegal day",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday"};
return((n<1||n>7) ? name[0] : name[n]);
}
   本程式是通過指標函式,輸入一個1~7之間的整數, 輸出對應的星期名。指標陣列的說明與使用一個陣列的元素值為指標則是指標陣列。 指標陣列是一組有序的指標的集合。 指標陣列的所有元素都必須是具有相同儲存型別和指向相同資料型別的指標變數。
   指標陣列說明的一般形式為: 型別說明符*陣列名[陣列長度]
   其中型別說明符為指標值所指向的變數的型別。例如: int *pa[3] 表示pa是一個指標陣列,它有三個陣列元素, 每個元素值都是一個指標,指向整型變數。通常可用一個指標陣列來指向一個二維陣列。 指標陣列中的每個元素被賦予二維陣列每一行的首地址, 因此也可理解為指向一個一維陣列。圖6—6表示了這種關係。
int a[3][3]={1,2,3,4,5,6,7,8,9};
int *pa[3]={a[0],a[1],a[2]};
int *p=a[0];
main(){
int i;
for(i=0;i<3;i++)
printf("%d,%d,%d\n",a[i][2-i],*a[i],*(*(a+i)+i));
for(i=0;i<3;i++)
printf("%d,%d,%d\n",*pa[i],p[i],*(p+i));
}
   本例程式中,pa是一個指標陣列,三個元素分別指向二維陣列a的各行。然後用迴圈語句輸出指定的陣列元素。其中*a[i]表示i行0列元素值;*(*(a+i)+i)表示i行i列的元素值;*pa[i]表示i行0列元素值;由於p與a[0]相同,故p[i]表示0行i列的值;*(p+i)表示0行i列的值。讀者可仔細領會元素值的各種不同的表示方法。 應該注意指標陣列和二維陣列指標變數的區別。 這兩者雖然都可用來表示二維陣列,但是其表示方法和意義是不同的。

  二維陣列指標變數是單個的變數,其一般形式中"(*指標變數名)"兩邊的括號不可少。而指標陣列型別表示的是多個指標( 一組有序指標)在一般形式中"*指標陣列名"兩邊不能有括號。例如: int (*p)[3];表示一個指向二維陣列的指標變數。該二維陣列的列數為3或分解為一維陣列的長度為3。 int *p[3] 表示p是一個指標陣列,有三個下標變數p[0],p[1],p[2]均為指標變數。

  指標陣列也常用來表示一組字串, 這時指標陣列的每個元素被賦予一個字串的首地址。 指向字串的指標陣列的初始化更為簡單。例如在例6.20中即採用指標陣列來表示一組字串。 其初始化賦值為:
char *name[]={"Illagal day",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday"};
   完成這個初始化賦值之後,name[0]即指向字串"Illegal day",name[1]指?quot;Monday"......。

  指標陣列也可以用作函式引數。在本例主函式中,定義了一個指標陣列name,並對name 作了初始化賦值。其每個元素都指向一個字串。然後又以name 作為實參呼叫指標型函式day name,在呼叫時把陣列名 name 賦予形參變數name,輸入的整數i作為第二個實參賦予形參n。在day name函式中定義了兩個指標變數pp1和pp2,pp1被賦予name[0]的值(即*name),pp2被賦予name[n]的值即*(name+ n)。由條件表示式決定返回pp1或pp2指標給主函式中的指標變數ps。最後輸出i和ps的值。

指標陣列作指標型函式的引數
main(){
static char *name[]={ "Illegal day",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday"};
char *ps;
int i;
char *day name(char *name[],int n);
printf("input Day No:\n");
scanf("%d",&i);
if(i<0) exit(1);
ps=day name(name,i);
printf("Day No:%2d-->%s\n",i,ps);
}
char *day name(char *name[],int n)
{
char *pp1,*pp2;
pp1=*name;
pp2=*(name+n);
return((n<1||n>7)? pp1:pp2);
}
下例要求輸入5個國名並按字母順序排列後輸出。在以前的例子中採用了普通的排序方法, 逐個比較之後交換字串的位置。交換字串的物理位置是通過字串複製函式完成的。 反覆的交換將使程式執行的速度很慢,同時由於各字串(國名) 的長度不同,又增加了儲存管理的負擔。 用指標陣列能很好地解決這些問題。把所有的字串存放在一個陣列中, 把這些字元陣列的首地址放在一個指標陣列中,當需要交換兩個字串時, 只須交換指標陣列相應兩元素的內容(地址)即可,而不必交換字串本身。程式中定義了兩個函式,一個名為sort完成排序, 其形參為指
針陣列name,即為待排序的各字串陣列的指標。形參n為字串的個數。另一個函式名為print,用於排序後字串的輸出,其形參與sort的形參相同。主函式main中,定義了指標陣列name 並作了初始化賦值。然後分別呼叫sort函式和print函式完成排序和輸出。值得說明的是在sort函式中,對兩個字串比較,採用了strcmp 函式,strcmp函式允許參與比較的串以指標方式出現。name[k]和name[ j]均為指標,因此是合法的。字串比較後需要交換時, 只交換指標陣列元素的值,而不交換具體的字串, 這樣將大大減少時間的開銷,提高了執行效率。
現程式設計如下:
#include"string.h"
main(){
void sort(char *name[],int n);
void print(char *name[],int n);
static char *name[]={ "CHINA","AMERICA","AUSTRALIA",
"FRANCE","GERMAN"};
int n=5;
sort(name,n);
print(name,n);
}
void sort(char *name[],int n){
char *pt;
int i,j,k;
for(i=0;i<n-1;i++){
k=i;
for(j=i+1;j<n;j++)
if(strcmp(name[k],name[j])>0) k=j;
if(k!=i){
pt=name[i];
name[i]=name[k];
name[k]=pt;
}
}
}
void print(char *name[],int n){
int i;
for (i=0;i<n;i++) printf("%s\n",name[i]);
}

main函式的引數

  前面介紹的main函式都是不帶引數的。因此main 後的括號都是空括號。實際上,main函式可以帶引數,這個引數可以認為是 main函式的形式引數。C語言規定main函式的引數只能有兩個, 習慣上這兩個引數寫為argc和argv。因此,main函式的函式頭可寫為: main (argc,argv)C語言還規定argc(第一個形參)必須是整型變數,argv( 第二個形參)必須是指向字串的指標陣列。加上形參說明後,main函式的函式頭應寫為:
main (argc,argv)
int argv;
char *argv[];或寫成:
main (int argc,char *argv[])
   由於main函式不能被其它函式呼叫, 因此不可能在程式內部取得實際值。那麼,在何處把實參值賦予main函式的形參呢? 實際上,main函式的引數值是從作業系統命令列上獲得的。當我們要執行一個可執行檔案時,在DOS提示符下鍵入檔名,再輸入實際引數即可把這些實參傳送到main的形參中去。

  DOS提示符下命令列的一般形式為: C:\>可執行檔名 引數 引數……; 但是應該特別注意的是,main 的兩個形參和命令列中的引數在
位置上不是一一對應的。因為,main的形參只有二個,而命令列中的引數個數原則上未加限制。argc參數列示了命令列中引數的個數(注意:檔名本身也算一個引數),argc的值是在輸入命令列時由系統按實際引數的個數自動賦予的。例如有命令列為: C:\>E6 24 BASIC dbase FORTRAN由於檔名E6 24本身也算一個引數,所以共有4個引數,因此argc取得的值為4。argv引數是字串指標陣列,其各元素值為命令列中各字串(引數均按字串處理)的首地址。 指標陣列的長度即為引數個數。陣列元素初值由系統自動賦予。其表示如圖6.8所示:
main(int argc,char *argv){
while(argc-->1)
printf("%s\n",*++argv);
}
本例是顯示命令列中輸入的引數如果上例的可執行檔名為e24.exe,存放在A驅動器的盤內。
因此輸入的命令列為: C:\>a:e24 BASIC dBASE FORTRAN
則執行結果為:
BASIC
dBASE
FORTRAN
   該行共有4個引數,執行main時,argc的初值即為4。argv的4個元素分為4個字串的首地址。執行while語句,每迴圈一次 argv值減1,當argv等於1時停止迴圈,共迴圈三次, 因此共可輸出三個引數。在printf函式中,由於列印項*++argv是先加1再列印, 故第一次列印的是argv[1]所指的字串BASIC。第二、 三次迴圈分別列印後二個字串。而引數e24是檔名,不必輸出。

  下例的命令列中有兩個引數,第二個引數20即為輸入的n值。在程式中*++argv的值為字串“20”,然後用函式"atoi"把它換為整型作為while語句中的迴圈控制變數,輸出20個偶數。
#include"stdlib.h"
main(int argc,char*argv[]){
int a=0,n;
n=atoi(*++argv);
while(n--) printf("%d ",a++*2);
}
   本程式是從0開始輸出n個偶數。指向指標的指標變數如果一個指標變數存放的又是另一個指標變數的地址, 則稱這個指標變數為指向指標的指標變數。

  在前面已經介紹過,通過指標訪問變數稱為間接訪問, 簡稱間訪。由於指標變數直接指向變數,所以稱為單級間訪。 而如果通過指向指標的指標變數來訪問變數則構成了二級或多級間訪。在C語言程式中,對間訪的級數並未明確限制, 但是間訪級數太多時不容易理解解,也容易出錯,因此,一般很少超過二級間訪。 指向指標的指標變數說明的一般形式為:
型別說明符** 指標變數名;
例如: int ** pp; 表示pp是一個指標變數,它指向另一個指標變數, 而這個指標變數指向一個整型量。下面舉一個例子來說明這種關係。
main(){
int x,*p,**pp;
x=10;
p=&x;
pp=&p;
printf("x=%d\n",**pp);
}
   上例程式中p 是一個指標變數,指向整型量x;pp也是一個指標變數, 它指向指標變數p。通過pp變數訪問x的寫法是**pp。程式最後輸出x的值為10。通過上例,讀者可以學習指向指標的指標變數的說明和使用方法。

  下述程式中首先定義說明了指標陣列ps並作了初始化賦值。 又說明了pps是一個指向指標的指標變數。在5次迴圈中, pps 分別取得了ps[0],ps[1],ps[2],ps[3],ps[4]的地址值(如圖6.10所示)。再通過這些地址即可找到該字串。
main(){
static char *ps[]={ "BASIC","DBASE","C","FORTRAN",
"PASCAL"};
char **pps;
int i;
for(i=0;i<5;i++){
pps=ps+i;
printf("%s\n",*pps);
}
}
本程式是用指向指標的指標變數程式設計,輸出多個字串。

本章小結
1. 指標是C語言中一個重要的組成部分,使用指標程式設計有以下優點:
(1)提高程式的編譯效率和執行速度。
(2)通過指標可使用主調函式和被調函式之間共享變數或資料結構,便於實現雙向資料通訊。
(3)可以實現動態的儲存分配。
(4)便於表示各種資料結構,編寫高質量的程式。

2. 指標的運算
(1)取地址運算子&:求變數的地址
(2)取內容運算子*:表示指標所指的變數
(3)賦值運算
·把變數地址賦予指標變數
·同型別指標變數相互賦值
·把陣列,字串的首地址賦予指標變數
·把函式入口地址賦予指標變數
(4)加減運算
對指向陣列,字串的指標變數可以進行加減運算,如p+n,p-n,p++,p--等。對指向同一陣列的兩個指標變數可以相減。對指向其它型別的指標變數作加減運算是無意義的。
(5)關係運算
指向同一陣列的兩個指標變數之間可以進行大於、小於、 等於比較運算。指標可與0比較,p==0表示p為空指標。

3. 與指標有關的各種說明和意義見下表。
int *p;     p為指向整型量的指標變數
int *p[n];   p為指標陣列,由n個指向整型量的指標元素組成。
int (*p)[n];  p為指向整型二維陣列的指標變數,二維陣列的列數為n
int *p()    p為返回指標值的函式,該指標指向整型量
int (*p)()   p為指向函式的指標,該函式返回整型量
int **p     p為一個指向另一指標的指標變數,該指標指向一個整型量。

4. 有關指標的說明很多是由指標,陣列,函式說明組合而成的。
但並不是可以任意組合,例如陣列不能由函式組成,即陣列元素不能是一個函式;函式也不能返回一個陣列或返回另一個函式。例如
int a[5]();就是錯誤的。

5. 關於括號
在解釋組合說明符時, 識別符號右邊的方括號和圓括號優先於識別符號左邊的“*”號,而方括號和圓括號以相同的優先順序從左到右結合。但可以用圓括號改變約定的結合順序。

6. 閱讀組合說明符的規則是“從裡向外”。
從識別符號開始,先看它右邊有無方括號或園括號,如有則先作出解釋,再看左邊有無*號。 如果在任何時候遇到了閉括號,則在繼續之前必須用相同的規則處理括號內的內容。例如:
int*(*(*a)())[10]
↑ ↑↑↑↑↑↑
7 6 4 2 1 3 5
上面給出了由內向外的閱讀順序,下面來解釋它:
(1)識別符號a被說明為;
(2)一個指標變數,它指向;
(3)一個函式,它返回;
(4)一個指標,該指標指向;
(5)一個有10個元素的陣列,其型別為;
(6)指標型,它指向;
(7)int型資料。
因此a是一個函式指標變數,該函式返回的一個指標值又指向一個指標陣列,該指標陣列的元素指向整型量。


相關文章