第八章 指標
8-1 指標的基本語法
8-1-1 指標與位址
8-1-2 取址運算&與間接運算*
8-1-3 指標常數與指標變數
8-1-4 指標的加減運算
8-1-5 指標的用途
8-2 指標與函數
8-2-1 傳址呼叫--用指標傳入函數
8-2-2 用指標傳回多個傳回值
8-2-3 指標型態的傳回值
8-3 指標與陣列
8-3-1 指向陣列的指標
8-3-2 用指標傳陣列資料入函數
8-3-3 用指標傳回陣列資料
8-4 指標的進階用法
8-4-1 指標陣列
8-4-2 雙重指標
8-4-3 函數指標
8-4-4 遠程指標(far)與近程指標(near)
8-4-5 動態記憶體配置
8-5 程式觀摩
第八章 習 題
第八章 指標
指標 (Pointer) 是C語言重要而特別的一項課題,適當地運用指標可以增加程式的執行效率,對許多與硬體相關的程式設計,更是不可或缺的重要工具。由於指標可以直接存取到記憶體的任何位置,所以功能很強,但是如果使用不當,就可能會造成破壞系統而導致當機的嚴重後果,對指標的用法,應該特別留意。
初學指標的人,通常不容易學會其中的觀念,其實只要掌握最重要的基本概念,多熟悉語法的運用,指標並不是非常難學的。
本章先從指標的基本語法談起,一一介紹運用指標的基本概念,各項運算與用途,請各位留心符號的寫法,多加練習。我們針對最常用的指標與函數、指標與陣列等主題,深入探討,讀者應熟悉這些指標的基本應用。至於指標的進階用法,如雙重指標、指標陣列、函數指標、遠程指標與近程指標、動態記憶體配置等較難的主題,可視情形自行斟酌。
8-1 指標的基本語法
「指標就是位址」,這句話可以說是本章最重要的基本觀念。
指標是C語言用來表示位址的一種語法,透過指標可以直接存取到任何的記憶體位址,我們把指標指向欲處理的資料位址,就可以方便地對那個資料進行操作。如果那個位址儲存的是一堆相關的資料,例如一個陣列資料,只須使用一個指標就可以讀取或寫入全部的資料。另外指標也被用來做不同程式段的資料交換,最常見的就是將資料傳入與傳出函數之內。
以下,我們從指標最基本的概念談起。
8-1-1 指標與位址
「指標就是位址」,意思是說指標資料型態的值就是某個記憶體位址。
例如我們宣告
int a;
int *p=&a;
表示 a 是一個整數變數的名稱,其資料型態是 int,而變數 a 是存在記憶體的某個地方,它的內容是一個"整數值",&a 就是這個地方的位址。
p 是一個整數指標變數(通常簡稱為整數指標)的名稱,其資料型態是 int *,既然是變數所以 p 也是存在記憶體的某個地方,但是它的內容是一個"記憶體位址",請注意 p 前面有個星號 *。我們宣告初值 p=&a 就是把 a 的位址,存入指標變數 p,換句話說就是 "p 指向 a":接下來就可以使用 p 指標,直接存取到整數變數 a 的內容。
因為 p 是指標變數,當然 &p 也表示這個變數的位址,只不過很少用到就是了。
*p 則是表示 "p所指資料的內容",就本例而言,因為 p 指向 a,其實 *p 就是 a 的內容值。
同理
char c, *cp;
cp = &c;
就是宣告字元變數 c 與字元指標 cp,而 cp = &c 就是把 c 的位址,存入指標變數 p,換句話說就是 "p 指向 c"。
8-1-2 取址運算&與間接運算*
指標的基本用法有兩個相關的運算符號:取址運算 & 與間接運算 *
先來看看 & 取址運算:
這個 & 運算符號的意義,就是取得變數的位址,所以稱為"取址運算"。
其實我們用來輸入整數的 scanf("%d",&a); 就是這個 & 運算符號。
而間接運算 * 是用取得"指標所指向的變數值",變數資料不是直接取得而是經由指標來間接(indirect)存取。
因為 p 指向 a,所以 *p 就是 a 的內容,如果
*p = 100;
則 a 的值就會變成 100,
而 printf("%d %d",a, *p); 就會印出 100 100 的結果。
【語法練習 8-1-1】
(1) 試宣告一個整數變數 x ________________________
(2) 試宣告一個整數指標 p ________________________
(3) 將 p 指向 x ________________________
(4) 印出 p 所指的內容 _______________________
(5) 印出 x 的位址 ________________________
8-1-3 指標常數與指標變數
指標常數是指直接指定的記憶體位址,例如 0xA0000000L 是 IBM PC VGA 卡顯示區的記憶體位址。
有一個常常用到的指標常數是 NULL,稱為"虛指標",代表不指向任何資料。
通常我們用 NULL 指標來做一連串資料的結束,或是表示所指向的資料是不存在的。
指標變數是指一個變數的內容是指標,也就是儲存指向某資料的位址;那個位址存的是整數,這個指標就是整數指標;那個位址存的是字元,這個指標就是字元指標。
其實不管是整數指標還是字元指標,對指標的內容而言,都一樣是位址,所差異的只是存取指標所指的內容時,一次是取一個整數為單位,或是一次取一個字元。因此我們可以用型態轉換的方式,直接變換指標的型態。例如:
int a, *ap;
char c, *cp;
a = 258;
ap = &a;
cp = (char *) ap;
c = *cp;
注意 ap 的資料型態是 int *,而 cp 的資料型態是 char *,同樣是指標,內容都是位址,所以指定型態轉換 (char *) 之後,就可以直接存入。這個例子的功能是將整數 a 的第一個位元組,存入字元 c。
有時候我們會遇到不指定資料型態的指標,寫成 void * 的宣告型態,表示暫時不指定資料型態,由程式設計者自己用型態轉換來指定,視當時的需要再決定。通常都是用在 malloc 或 calloc 等記憶体配置函數的宣告。
指標型態的列印,最好使用 printf 的 %p 格式,這是列印出完整的"位址"。如果編譯程式不是提供 %p 的格式,也可以用 %u 的格式來印。例如:
int num;
int *p=#
num=18;
printf("*p=%d p=%p &p=%p", *p, p, &p);
分別印出指標所指的值 *p,指標本身的內容 p,以及指標變數所在的位址 &p。
指標變數本身所佔的記憶體位元組數,可以用 sizeof(p) 來求出,而 sizeof(*p) 是代表指標所指變數一個單位的大小。
【語法練習 8-1-2】
(1) 試宣告一個字元變數 x ________________________
(2) 試宣告一個字元指標 p ________________________
(3) 將 p 指向 x ________________________
(4) 印出 p 所指的內容 _______________________
(5) 印出 p 所佔的記憶體位元組數 ________________________
8-1-4 指標的加減運算
指標的型態宣告會指定所指向的資料,一次存取的單位。整數指標一次取一個整數,而字元指標一次取一個字元為單位。因此,指標的加法和減法運算,也是每次加一個單位或減一個單位的位址。對整數指標而言,加一個單位就是增加 2 個位元組的位址。如果 p 是整數指標 int *p,其位址是 1000
則 p+1 就是位 1002,p+2 就是位址 1004,而 p+i 是 p 往後的第 i 個,位址為
1000+i*sizeof(int) = 1000+i*2
對其他型態的資料,也可仿此類推。
指標的遞加運算 ++ 就是每次指向下一個單位的資料,遞減運算 -- 就是指向前一個單位的資料,通常是處理陣列型態的資料時,才會派上用場。
【範例 8-1-1】
指標的間接運算*與取址運算&
1 /* Example 8-1-1 */
2 /* pointer operator & and * */
3
4 main()
5 {
6 int i,*ptr;
7
8 i=100;
9 printf("i=%d &i=%p\n",i,&i);
10 ptr=&i;
11 printf("*ptr=%d ptr=%p\n",*ptr,ptr);
12
13 *ptr=99;;
14 printf("*ptr=%d i=%d\n",*ptr,i);
15 }
執行結果:
i=100 &i=0CD7:0FFE
*ptr=100 ptr=0CD7:0FFE
*ptr=99 i=99
【範例 8-1-2】
指標的遞加運算++與遞減運算--
1 /* Example 8-1-2 */
2 /* pointer ++ and -- */
3 #include <conio.h>
4
5 main()
6 {
7 int *ip;
8 char *cp;
9 long *lp;
10
11 clrscr();
12 printf("address of ip = %p\n",ip);
13 printf("address of ip+1 = %p\n",ip+1);
14 printf("address of ip+2 = %p\n",ip+2);
15 ip++;
16 printf("after ip++, address of ip = %p\n",ip);
17
18 printf("address of cp = %p\n",cp);
19 printf("address of cp+1 = %p\n",cp+1);
20 printf("address of cp+2 = %p\n",cp+2);
21 cp++;
22 printf("after cp++, address of cp = %p\n",cp);
23
24
25 printf("address of lp = %p\n",lp);
26 printf("address of lp+1 = %p\n",lp+1);
27 printf("address of lp+2 = %p\n",lp+2);
28 lp--;
29 printf("after lp--, address of lp = %p\n",lp);
30
31 }
執行結果:
address of ip = 0BA9:0720
address of ip+1 = 0BA9:0722
address of ip+2 = 0BA9:0724
after ip++, address of ip = 0BA9:0722
address of cp = 0003:5003
address of cp+1 = 0003:5004
address of cp+2 = 0003:5005
after cp++, address of cp = 0003:5004
address of lp = FFEA:049C
address of lp+1 = FFEA:04A0
address of lp+2 = FFEA:04A4
after lp--, address of lp = FFEA:0498
【範例 8-1-3】
用指標存取陣列的元素
1 /* Example 8-1-3 */
2 /* pointer to array elements */
3 #include <conio.h>
4
5 main()
6 {
7 int a[5]={9,8,7,6,5};
8 int *ip;
9
10 clrscr();
11 ip=a;
12 printf("*ip = %d\n",*ip);
13 printf("*(ip+1) = %d\n",*(ip+1));
14 printf("*(ip+2) = %d\n",*(ip+2));
15 printf("*(ip+3) = %d\n",*(ip+3));
16 printf("*(ip+4) = %d\n",*(ip+4));
17 }
執行結果:
*ip = 9
*(ip+1) = 8
*(ip+2) = 7
*(ip+3) = 6
*(ip+4) = 5
8-1-5 指標的用途
指標是C語言功能很強的語法,通常有下列的用途:
1‧傳遞函數的引數
由於函數內部無法直接存取函數外的資料,可以將資料的位址以指標型態傳入函數內,這樣就可以間接存取到函數外的資料,這就是所謂的傳址呼叫 (call by address) 的觀念。
我們可以用這種方式來傳回多個函數內部的執行結果,也可以把資料處理完後,將其位址以指標型態來傳回。
2‧有效率地存取陣列資料
當指標指向一個陣列時,陣列元素的存取就可用指標間接運算來進行,由於指標是直接操作記憶体位址,可用比較接近機器指令的方式來運算,所以執行的速度較快,效率較高。此外,指標也用來將陣列的起始位址傳入函數,而不用傳入整個陣列的資料,簡化函數呼叫的執行工作。
3‧直接記憶體定址
用指標直接指向記憶體位址,就可以直接控制 VGA 螢幕或其他硬體裝置,這是比較接近低階語言的用法。此外,也可以用指標傳遞系統呼叫 (system call) 的各項參數。
4‧複雜的資料型態
許多資料結構所使用的複雜資料型態,例如鏈結串列 (linked list)、樹狀結構 (tree structure) 等,就需要用指標型態才能達成。
5‧動態記憶體配置
當程式執行時,視需要向系統要求配置記憶體空間,來存放資料,就是所謂的動態記憶體配置 (dynamic memory allocation)。等到資料處理完後,程式釋放記憶體還給系統,下次有需求時再要求配置,這樣可以充份利用系統所剩餘的記憶體空間,讓程式可以處理更大量的資料。
以下各節,我們將討論一些指標最重要的應用。
8-2 指標與函數
指標常見的用途之一,就是傳遞函數的引數,以及傳回函數執行的結果。本節討論指標與函數間的相關應用。
8-2-1 傳址呼叫--用指標傳入函數
C語言程式使用函數呼叫時,會把實際引數的值複製一份傳給型式引數,例如:
int sum_sq ();
main()
{
int a=3,b=4,c;
c=sum_sq(a,b);
printf("sum of square %d %d is %d",a,b,c);
}
int sum_sq (int p,int q)
{
int r;
r=p*p+q*q;
return r;
}
主程式呼叫 sum_sq 函數時,實際引數 a,b 的值會分別拷貝給函數內的型式引數 p,q 。這種複製引數值的函數呼叫,稱為傳值呼叫 (call by value) 。函數內的 p 與 q 的值即使有所改變,也不會影響到函數外 a,b 的值。這是因為變數 a,b 的勢力範圍只有在主程式中,而變數 p,q 的勢力範圍只在函數中。
但是有時候,我們想在函數內直接改變函數外的資料時,這種方式就無法達成目的。
【範例 8-2-1】 用傳址呼叫無法將兩變數的值互換
解決的方法就是將變數的位址傳入函數內,這樣函數內藉由位址來間接存取這兩個變數,就可以實際改變其內容。這種將位址傳入出數的方式,就稱為傳址呼叫 ( call by address)。傳址呼叫時,將實際引數的位址帶入,而函數內用指標變數來承接。
1 /* Example 8-2-1 */
2 /* swap() call by value : wrong version */
3
4 void swap (int,int);
5
6 main()
7 {
8 int a=100,b=99;
9
10 printf("before swap(): a=%d b=%d\n",a,b);
11 swap(a,b);
12 printf("after swap(): a=%d b=%d\n",a,b);
13 }
14
15 void swap (int p, int q)
16 {
17 int t;
18
19 printf("before exchange: p=%d q=%d\n",p,q);
20 t=p;
21 p=q;
22 q=t;
23 printf("after exchange: p=%d q=%d\n",p,q);
24 }
執行結果:
before swap(): a=100 b=99
before exchange: p=100 q=99
after exchange: p=99 q=100
after swap(): a=100 b=99
【範例 8-2-2】 用傳址呼叫來將兩個變數的值互換
1 /* Example 8-2-2 */
2 /* swap() call by address : correct version */
3
4 void swap (int *,int *);
5
6 main()
7 {
8 int a=100,b=99;
9
10 printf("before swap(): a=%d b=%d\n",a,b);
11 swap(&a,&b);
12 printf("after swap(): a=%d b=%d\n",a,b);
13 }
14
15 void swap (int *p, int *q)
16 {
17 int t;
18
19 printf("before exchange: *p=%d *q=%d\n",*p,*q);
20 t=*p;
21 *p=*q;
22 *q=t;
23 printf("after exchange: *p=%d *q=%d\n",*p,*q);
24 }
執行結果:
before swap(): a=100 b=99
before exchange: *p=100 *q=99
after exchange: *p=99 *q=100
after swap(): a=99 b=100
8-2-2 用指標傳回多個傳回值
一個函數只能用 return 來傳回一個傳回值。如果函數內運算的結果,得到一個以上的數值,就必須另覓他法。
一種方法是採用全域變數,定義在所有函數內的外面。函數內的所有運算結果,可以直接存入全域變數,欲傳入出數內的引數值,也可以先存入全域變數,然後在函數內直接存取。
【範例 8-2-3】
用全域變數來傳回多個運算結果--商與餘數
1 /* Example 8-2-3 */
2 /* return quotient and remainder by global variables */
3
4 int quo, rem; /* global variables */
5
6 main()
7 {
8 void divide2(int,int);
9 int a=7,b=3;
10
11 divide2(a,b);
12 printf("quotient of %d divided by %d is %d\n",a,b,quo);
13 printf("remainder of %d divided by %d is %d\n",a,b,rem);
14 }
15
16 void divide2 (int p, int q)
17 {
18 quo = p/q;
19 rem = p%q;
20 }
執行結果:
quotient of 7 divided by 3 is 2
remainder of 7 divided by 3 is 1
用全域變數來傳函數的傳回值,雖然很方便,但是要付出一些代價。其一是全域變數是所有函數都存取得到,非屬本函數專用,可能會被其他函數所更動,導致錯誤的結果。其二是程式如果使用太多全域變數,管理起來較費事,而且函數內部有函數外部的資料,會破壞函數的獨立性。此外,全域變數所佔用的記憶體空間無法釋放,不像函數的引數是在堆疊中,呼叫時才佔用記憶體,函數返回時,就會釋放。
用指標來傳回多個傳回值,也是一種可行的方法。這樣讓傳遞資料的呼叫者,與函數內部的執行結果,直接對映,不會受到其他程式片段或函數的影響。
【範例 8-2-4】
用指標來傳回多個運算結果--商與餘數
1 /* Example 8-2-4 */
2 /* return quotient and remainder by pointer arguments */
3
4 main()
5 {
6 void divide2(int,int,int *,int *);
7 int a=7,b=3,quo,rem;
8
9 divide2(a,b,&quo,&rem);
10 printf("quotient of %d divided by %d is %d\n",a,b,quo);
11 printf("remainder of %d divided by %d is %d\n",a,b,rem);
12 }
13
14 void divide2 (int p, int q, int *qval, int *rval)
15 {
16 *qval = p/q;
17 *rval = p%q;
18 }
執行結果:
quotient of 7 divided by 3 is 2
remainder of 7 divided by 3 is 1
8-2-3 指標型態的傳回值
有時候函數運算的結果是一項位址,例如一個字串的位址,一個陣列的起始位址等,這時就必須使用指標型態的傳回值。
【範例 8-2-5】
設計一個函數輸入一個字串,傳回字串中第一個空白符號 ( ' '、'\t'、或 '\n' ) 出現的位址,若找不到則傳回虛指標 NULL。字串是以 '\0' 字元做為結束。
1 /* Example 8-2-5 */
2 /* get address of white space in a string */
3 #include <stdio.h>
4
5 char *get_white (char *);
6
7 main()
8 {
9 char msg1[20]="Merry Xmas";
10 char msg2[20]="line1\nline2\n";
11 char msg3[20]="ChenHongShan";
12 char *s1,*s2,*s3;
13
14 s1=get_white(msg1);
15 printf("msg1 : %s s1 : [%s]\n",msg1,s1);
16 s2=get_white(msg2);
17 printf("msg2 : %s s2 : [%s]\n",msg2,s2);
18 s3=get_white(msg3);
19 printf("msg3 : %s s3 : [%s]\n",msg3,s3);
20 }
21
22 char *get_white (char *s)
23 {
24 while (*s != '\0') {
25 if (*s == ' ' || *s == '\t' || *s == '\n')
26 return s;
27 s++;
28 };
29 return NULL;
30 }
執行結果:
msg1 : Merry Xmas s1 : [ Xmas]
msg2 : line1
line2
s2 : [
line2
]
msg3 : ChenHongShan s3 : [(null)]
8-3 指標與陣列
指標與陣列的使用,關係相當密切。陣列是一連串相同型態資料的集合,配置在連續的記憶體位址,用索引值來存取某個特定的元素。例如
int a[6],i;
for (i=0;i<6;i++)
a[i]=0;
將 a 整數陣列的 6 個元素都填成 0,其中 a[i] 表示 a 的第 i 個元素。若 a 的起始位址是 1000,則 &a[i] 是 a 的第 i 個元素的位址,就是 1000+i*sizeof(int),可以依此算出。
如果我們將指標指向一個陣列,那麼就可用指標的加減運算來存取陣列的某特定元素。特別是對陣列元素進行連續的存取時,例如列印陣列內容,存入陣列資料等,執行速度較快,因為指標是以記憶體位址來直接存取資料,所以效率較高。這就是為什麼會使用指標來存取陣列資料的原因。上面的例子可改用指標來達成:
int a[6],i,*p;
p=a;
for (i=0;i<6;i++)
*(p+i) = 0;
請注意 a 是陣列名稱,可代表陣列的起始位址,所以 p=a; 和 p=&a[0]; 是相同的效果,都是將p指向a陣列的開頭。而 p+i 是 p 加 i 個單位的位址,*(p+i) 就是那個元素的內容,所以 *(p+i) 就相當於 a[i],我們也可以直接寫成 p[i]。
如果談得詳細一點,a 陣列和 p 指標兩種用法有何不同呢?因 a 是陣列名稱,代表陣列的起始位址,是一個"常數",其值不可以改變,所以像 a++ 等這種運算是不合法的。而 p 是指標變數,內容為某一位址,其值是可變的,所以 p++ 就是 p 指向下一個元素,這是合法的運算。
其實上述範例用指標來做,最有效率的寫法如下:
int a[b],i,*p;
p=a;
for (i=0;i<b;i++)
*p++ = 0;
注意 *p++ 的作用是先將 p 所指的元素設定為 0 後,再將 p 指標的內容遞增一個單位,使 p 指向下一個元素。
8-3-1 指向陣列的指標
當一個指標變數指向一個陣列時,概念上就可以把指標當成是陣列來用,不管這個陣列型態是整數、字元或是其它更複雜的資料型態。例如:
int score[10],*p=score;
則我們可以用 p[i] 或 *(p+i) 來當成 score[i],其實也可以用 *(score+i) 來表示,只不過這種寫法沒有什麼意義,因為 score 本來就是一個陣列。
如果指標指向一個字串,通常會用如下宣告:
char *p="Help";
其實就是 p 指標指向一個以 '\0' 字元為結束的字元陣列,p[i] 或 *(p+i) 都是表示字串的第 i 個元素,列印整個字串可以用 printf("%s",p); 來表示,詳細的字串用法將在第九章完整地介紹。
【語法練習 8-3-1】
(1) 試宣告一個整數陣列 x ________________________
(2) 試宣告一個整數指標 p ________________________
(3) 將 p 指向 x 陣列 ________________________
(4) 印出 p 所指的第 0 個元素的內容 _______________________
(5) 用for 迴圈印出 p 所指的所有元素的內容
________________________________________
________________________________________
【語法練習 8-3-2】
(1) 試宣告一個字元陣列 x ________________________
(2) 試宣告一個字元指標 p ________________________
(3) 將 p 指向 x 陣列 ________________________
(4) 印出 p 所指的第 3 個元素的內容 _______________________
(5) 用for 迴圈印出 p 所指的所有元素的內容
________________________________________
________________________________________
【範例 8-3-1】
列印指標所指向的陣列
1 /* Example 8-3-1 */
2 /* print array by pointer */
3
4 #define N 6
5
6 main()
7 {
8 int a[N]={1,3,5,7,9,11};
9 int i, *p;
10
11 p=a;
12 for (i=0; i<N; i++)
13 printf("i=%d: %d, %d\n",i,a[i],*p++);
14 }
執行結果:
i=0: 1, 1
i=1: 3, 3
i=2: 5, 5
i=3: 7, 7
i=4: 9, 9
i=5: 11, 11
8-3-2 用指標傳陣列資料入函數
需要將一個陣列傳入函數時,不必將整個陣列的所有元素全部傳入,只須將陣列名稱(也就是陣列的起始位址)當成指標傳入函數即可,函數內部可藉由指標,直接存取陣列的各個元素。
以下,我們來看幾個基本的範例。
【範例 8-3-2】
使用指標來印出傳入函數的陣列資料
1 /* Example 8-3-2 */
2 /* print array function by pointer argument */
3
4 void print_array (int *,int);
5
6 main()
7 {
8 int a[]={1,3,5,7,9,11};
9 int n=sizeof(a)/sizeof(int);
10
11 print_array(a,n);
12 }
13 void print_array (int *p, int cnt)
14 {
15 int i;
16
17 for (i=0; i<cnt; i++)
18 printf("%dth element : %d\n",i,*p++);
19 }
執行結果:
0th element : 1
1th element : 3
2th element : 5
3th element : 7
4th element : 9
5th element : 11
【範例 8-3-3】
設計一函數,傳入一維整數陣列,及其元素個數,用指標方式來求陣列的總和,並設計一程式來加以驗証。
1 /* Example 8-3-3 */
2 /* summing array function by pointer argument */
3
4 int sum_array (int *,int);
5
6 main()
7 {
8 int a[]={1,3,5,7,9,11};
9 int n=sizeof(a)/sizeof(int);
10
11 printf("total = %d\n",sum_array(a,n));
12 }
13 int sum_array (int *p, int cnt)
14 {
15 int i,sum;
16
17 sum=0;
18 for (i=0; i<cnt; i++)
19 sum += *p++;
20 return sum;
21 }
執行結果:
total = 36
【範例 8-3-4】
設計一函數,傳入一維整數陣列,及其元素個數,用指標方式,求陣列元素的最大值,並設計一程式來加以驗証。
1 /* Example 8-3-4 */
2 /* get max value function by pointer argument */
3
4 int get_max (int *,int);
5
6 main()
7 {
8 int a[]={1,-3,5,-7,9,-11};
9 int n=sizeof(a)/sizeof(int);
10
11 printf("max value = %d\n",get_max(a,n));
12 }
13 int get_max (int *p, int cnt)
14 {
15 int max,i;
16
17 max=p[0];
18 for (i=1; i<cnt; i++)
19 if (p[i] > max) max=p[i];
20 return max;
21 }
執行結果:
max value = 9
8-3-3 用指標傳回陣列資料
如果函數的輸出是一個陣列,可以直接傳回指向陣列開頭的指標。不過要注意陣列的記憶體空間是否在函數外事先配置好,否則應指定陣列為 static 方可傳回,因為 static 宣告會將陣列配置在靜態變數區,資料才能保存。如果用自動變數,函數一結束,陣列空間就被全部回收了!
【範例 8-3-5】
設計一函數傳入一維整數陣列及元素個數,將其元素由小到大排序,傳回排序後的陣列指標。
1 /* Example 8-3-5 */
2 /* get sorted array */
3 #include <conio.h>
4
5 void print_array(int *,int);
6 int *sort_array (int *,int);
7
8 main()
9 {
10 int a[]={88,44,33,22,77,55,66};
11 int n=sizeof(a)/sizeof(int);
12 int *sa;
13
14 clrscr();
15 printf("array before sort ");
16 print_array(a,n);
17 sa = sort_array(a,n);
18 printf("array after sort ");
19 print_array(sa,n);
20 }
21
22 void print_array (int *p, int n)
23 {
24 int i;
25
26 for (i=0; i<n; i++)
27 printf("%d ",*p++);
28 printf("\n");
29 }
30
31 void swap (int *a, int *b)
32 {
33 int t;
34 t=*a, *a=*b, *b=t;
35 }
36
37 int *sort_array (int *p, int cnt)
38 {
39 int i,j,t;
40
41 for (j=cnt-1; j>0; j--) {
42 printf("Pass %d:\n",cnt-j);
43 for (i=0; i<j; i++)
44 if (p[i] > p[i+1]) {
45 printf("swap [%d]:[%d] -- ",p[i],p[i+1]);
46 swap(&p[i],&p[i+1]);
47 print_array(p,cnt);
48 }
49 }
50 return p;
51 }
執行結果:
array before sort 88 44 33 22 77 55 66
Pass 1:
swap [88]:[44] -- 44 88 33 22 77 55 66
swap [88]:[33] -- 44 33 88 22 77 55 66
swap [88]:[22] -- 44 33 22 88 77 55 66
swap [88]:[77] -- 44 33 22 77 88 55 66
swap [88]:[55] -- 44 33 22 77 55 88 66
swap [88]:[66] -- 44 33 22 77 55 66 88
Pass 2:
swap [44]:[33] -- 33 44 22 77 55 66 88
swap [44]:[22] -- 33 22 44 77 55 66 88
swap [77]:[55] -- 33 22 44 55 77 66 88
swap [77]:[66] -- 33 22 44 55 66 77 88
Pass 3:
swap [33]:[22] -- 22 33 44 55 66 77 88
Pass 4:
Pass 5:
Pass 6:
array after sort 22 33 44 55 66 77 88
【範例 8-3-6】
設計一函數 get_str 由鍵盤輸入一連串字元,直到 Enter 鍵為止,將其變成字串後,傳回指向字串的指標。
1 /* Example 8-3-6 */
2 /* get_str() */
3 char *get_str();
4
5 main()
6 {
7 char *msg;
8
9 msg=get_str();
10 printf("%s",msg);
11 }
12
13 char *get_str()
14 {
15 static char str[80]; /* allocate static data */
16 int i=0;
17 char *p=str;
18
19 *p=getchar();
20 while (*p != '\n') {
21 p++;
22 *p=getchar();
23 }
24 *p='\0'; /* make string */
25 return str;
26 }
執行結果:
test
test
8-4 指標的進階用法
接下來,我們談一些指標較深入的用法。
8-4-1 指標陣列
指標陣列就是一個陣列,每一個元素都是指標。如果指標指向整數,就稱為整數指標陣列。最常見的是指向一個字串,那麼這個陣列就稱為字串陣列,在許多文字處理的應用中,都會用到字串陣列,相關的詳細主題,留待第九章再介紹。
我們先來看看基本的指標陣列用法。
如果宣告一個整數指標陣列
int *a[5];
則記憶體會配置 5 個元素的陣列,每個元素都是一個整數指標。我們可以讓元素指向其他的整數變數,例如
int x;
a[1]=&x;
會使 a[1] 指向 x,那麼就可以用 *a[1] 來存取 x 的值。
我們常會使用字串陣列,例如
char *name[4]={"wang","lee","chen","kuo"};
宣告了4個元素的字串陣列,設定4個英文姓名字串為初值,中文的姓名也可以仿此運用。
請注意宣告 char *name[4] 與 char (*name)[4] 是不同的意義。因為 [] 運算的優先順序比 * 高,所以必須先處理 [] 運算, char *name[4] 表示 name 是一個陣列,共有 4 個元素,其每一個元素都是整數指標;而 char (*name)[4] 表示 name 是一個指標變數,指向一個 4 個元素的字元陣列。
【範例 8-4-1】
用字串陣列來存同學名單
1 /* Example 8-4-1 */
2 /* string array : name list */
3 #define N 5
4 char *name[N]= {"chen","lin","lee","tsai","hsee"};
5
6 main()
7 {
8 int i;
9
10 printf("name list :\n");
11 for (i=0; i<N; i++)
12 printf("%s\n",name[i]);
13 }
執行結果:
name list :
chen
lin
lee
tsai
hsee
【範例 8-4-2】
不定元素個數的字串陣列
1 /* Example 8-4-2 */
2 /* string array : name list2 */
3 #include <stdio.h>
4
5 char *name[]= { "Peter",
6 "Mary",
7 "Ben",
8 "Cook",
9 NULL
10 };
11
12 main()
13 {
14 int i;
15
16 printf("name list :\n");
17 i=0;
18 while (name[i] != NULL) {
19 printf("%s\n",name[i]);
20 i++;
21 }
22 printf("total %d names\n",i);
23 }
執行結果:
name list :
Peter
Mary
Ben
Cook
total 4 names
8-4-2 雙重指標
指標的內容是一個指向某項資料的位址,而雙重指標就是一個指標,其內容還是一個指標,也就是說 " 指向指標的指標 "。
例如
int x, *p, *pp;
p=&x;
pp=&p;
表示 p 是整數指標指向 x,而 pp 是雙重指標,指向 p。
【範例 8-4-3】
雙重指標的基本概念 a, *p=&a, **pp=&p
1 /* Example 8-4-3 */
2 /* pointer to pointer */
3
4 main()
5 {
6 int a,*p,**pp;
7
8 a=99;
9 p=&a;
10 pp=&p;
11 printf("a=%d &a=%p\n",a,&a);
12 printf("p=%p *p=%d &p=%p\n",p,*p,&p);
13 printf("pp=%p *pp=%p *(*pp)=%d\n",pp,*pp,*(*pp));
14 }
執行結果:
a=99 &a=0E52:0FFE
p=0E52:0FFE *p=99 &p=0E52:0FFA
pp=0E52:0FFA *pp=0E52:0FFE *(*pp)=99
如果宣告指標陣列指向一個二維整數陣列的各列:
int a[3][3], *ap[3], **pp;
ap[0]=a[0];
ap[1]=a[1];
ap[2]=a[2];
請注意 a 是二維陣列,a[0]是一維陣列名稱,代表第 0 列的開頭位址。
我們可以讓雙重指標 pp 指向指標陣列的某個元素,間接指向二維陣列的某一列。
例如
pp = &ap[1];
讓 pp 指向第1個指標元素,而 *pp 就是指向第一列的開頭位址。
如果我們仔細研究一下,就會發現二維陣列的名稱,其實就可視為一個雙重指標。
例如三列四行的二維整數陣列
int m[3][4];
其第 i 列第 j 行的元素為 m[i][j],如果用指標的寫法就是 *(*(m+i)+j)
這兩者是相同的。
而 m[i] 是一維陣列名稱,也就是第 i 列的起始位址,用指標的寫法是 *(m+i)
如果我們先將各列的起始位址存入指標陣列,例如
int *ap[3],m[3][3],i;
for (i=0; i<3; i++)
ap[i]=m[i];
這個指標陣列就是所謂的 lookup table。那麼,二維陣列元素的存取,就可以直接用指標存取,執行較快速。這種做法通常運用於螢幕繪圖的效果,以加快繪圖的速度。
【範例 8-4-4】
二維陣列名稱可視為雙重指標
1 /* Example 8-4-4 */
2 /* pointer to pointer : 2D array name */
3
4 main()
5 {
6 int a[3][3]={ {9,8,7}, {7,7,7}, {1,3,5} };
7 int i,j;
8
9 for (i=0; i<3; i++) {
10 for (j=0; j<3; j++)
11 printf("%d ",*(*(a+i)+j));
12 printf("\n");
13 }
14 }
執行結果:
9 8 7
7 7 7
1 3 5
8-4-3 函數指標
函數指標是一種特殊的指標,它不像一般的指標指向資料,而是指向某個函數的進入點位址。把指標指向函數名稱,就可以透過指標來呼叫函數,這是指標較高段的用法。
例如宣告
int (*func)();
表示 func 是一個函數指標,指向某個函數,而這個函數的傳回值是 int 型態。
如果有一個函數宣告為
int f1();
我們可以讓 func 指向 f1 函數,寫成 func=f1; 其中 f1 是函數的名稱,也就是該函數進入點的位址。要呼叫函數就使用
(*func)();
請注意 int *func(); 與 int (*func)(); 是不同的。
int *func(); 是說 func 是一個"函數",它的傳回值是一個"整數指標"。
int (*func)() 是說 func 是一個"函數指標",這個指標所指向的函數具有整數型態的傳回值。
【範例 8-4-5】
函數指標基本用法
1 /* Example 8-4-5 */
2 /* function pointer */
3
4 int square (int);
5 int cubic (int);
6 int nothing (int);
7
8 main()
9 {
10 int data,n,v;
11 int (*fp)();
12
13 printf("input number : ");
14 scanf("%d",&data);
15 printf("select function: 2 for square, 3 for cubic ;");
16 scanf("%d",&n);
17 if (n==2) fp=square;
18 else if (n==3) fp=cubic;
19 else fp=nothing;
20
21 v = (*fp)(data);
22 printf("result is %d\n",v);
23 }
24
25 int square (int x)
26 {
27 return x*x;
28 }
29 int cubic (int x)
30 {
31 return x*x*x;
32 }
33 int nothing (int x)
34 {
35 return 0;
36 }
執行結果:
input number : 4
select function: 2 for square, 3 for cubic ;2
result is 16
input number : 4
select function: 2 for square, 3 for cubic ;3
result is 64
8-4-4 遠程指標(far)與近程指標(near)
對 IBM PC 來說,C語言的指標有遠程指標與近程指標之分。因為 IBM PC 內容的記憶體位址包括節區(segment)與節區內偏移位址(offset)的兩部份。如果指標所指向的資料位址,是可以跨越目前節區的位址,那麼這種指標就是遠程指標,必須在指標宣告時用 far 來指定:如果指標所指位址,是在目前節區之內則是近程指標。
例如:
char far *p1;
int far *p2;
分別指定 p1 與 p2,為遠程指標指向字元資料與整數資料。如果不指定為 far,則指標是近程或遠程由編譯時的記憶體模式而定,通常用 small 模式編譯會內定為近程指標,若用 large 模式則內定為遠程指標。請參考附錄E的說明。
一般而言,會用到遠程指標的程式,主要是向系統要求記憶體配置,或是為了直接存取特定位址的資料。
8-4-5 動態記憶體配置
一般的陣列或其他的資料變數,在宣告時就會被配置記憶體空間,不管這些變數是否全部被使用到。例如宣告一個陣列
int score[100];
來儲存本班同學的C語言成績,預留最大 100 筆資料,不管本班同學人數是 50 或 60,這個100筆資料佔用 100*sizeof(int)=200 個位元組的記憶體,其他未使用的元素就算是浪費掉了。這種在編譯時,就事先配置記憶體的方式,稱為靜態記憶體配置(static memory allocation)。
較聰明的做法是,“用多少才配置多少”,在程式執行時,才實際配置記憶體的空間,這就是所謂的動態記憶體配置(dynamic memory allocation)。使用動態記憶體配置,須用指標來指向新配置的資料位置,通常用以下兩種記憶體配置函數:
# include <alloc.h>
# include <stdlib.h>
void *calloc (n, esize)
void *malloc (n);
其中 n 是元素個數,esize 是每個元素佔的位元組數,函數呼叫的範例如
int *p1;
p1=(int *) calloc(100,sizeof(int));
:
:
free(p1);
呼叫 calloc 向系統要求 100 個整數的資料,用(int *)型態轉換成 p1 指標的型態,接下來就可以把 p1 當成 100 個元素的整數陣列來用。當這個陣列不再使用時,可呼叫 free 函數來將記憶體還給系統,下次有其他記憶體需求時,再向系統要求配置。
再看:
char *buffer;
buffer= (char *) malloc(4096);
:
:
free(buffer);
呼叫 malloc 要求配置 4k 大小的記憶體,轉換型態成 char * 指標,用完之後再呼叫 free 函數釋放回系統。
請注意必須 #include <alloc.h> 或 <stdlib.h> 標頭檔。
【範例 8-4-6】
calloc 的用法
1 /* Example 8-4-6 */
2 /* calloc() */
3 #include <stdio.h>
4 #include <alloc.h>
5 #include <stdlib.h>
6 #define N 5
7
8 main()
9 {
10 int *p,i;
11
12 p = (int *) calloc( N, sizeof(int));
13 if (p==NULL) {
14 printf("calloc() error");
15 exit(1);
16 }
17 for (i=0; i<N; i++)
18 p[i] = i*i;
19 for (i=0; i<N; i++)
20 printf("%d\n",*(p+i));
21 free(p);
22 }
執行結果:
0
1
4
9
16
【範例 8-4-7】
malloc 的用法
1 /* Example 8-4-7 */
2 /* malloc() */
3 #include <stdio.h>
4 #include <alloc.h>
5 #include <stdlib.h>
6 #define N 1024
7
8 main()
9 {
10 char *p;
11 int i;
12
13 p = (char *) malloc(N);
14 if (p==NULL) {
15 printf("malloc() error");
16 exit(1);
17 }
18 for (i=0; i<N; i++)
19 p[i] = 'H';
20 for (i=0; i<16; i++)
21 printf("%c",*(p+i));
22 free(p);
23 }
執行結果:
HHHHHHHHHHHHHHHH
8-5 程式觀摩
【範例 8-5-1】
直接存取 VGA 螢幕的顯示記憶體位址,製作捲軸動畫。
請設定 TurboC++ 的 Option 功能表選項如下:
Options/Complier/Source/TurboC++
因為 ANSI C 並不支援 far 遠程指標的宣告。
1 /* Example 8-5-1 */
2 /* VGA address direct access */
3 /* set Options/Complier/Source/TurboC++ */
4 #include <conio.h>
5 #include <stdio.h>
6 #include <mem.h>
7 #include <dos.h>
8 #define ROW 25
9 #define COL 80
10
11 char far *vga_buf = (char far *) 0xB8000000L; /* VGA text screen */
12
13 main()
14 {
15 char far *p[ROW];
16 char icon[3][6]= { " * ",
17 " *** ",
18 "*****",
19 };
20 int i,x,y,start;
21 char far *t;
22
23 /* bulid look up address table */
24 for (i=0; i<ROW; i++)
25 p[i]= vga_buf+i*COL*2; /* 2 bytes for attribute and character */
26
27 while (!kbhit()) {
28 /* clear scrren */
29 for (i=0; i<ROW; i++)
30 memset(p[i],0x0000,COL*2);
31 /* show icon */
32 start=ROW-3;
33 for (y=0; y<3; y++)
34 for (x=0; x<6; x++) {
35 *(p[start+y]+2*x) = icon[y][x]; /* character symbol */
36 *(p[start+y]+2*x+1) = RED; /* character attribute */
37 }
38 /* rotate look up table */
39 t=p[0];
40 for (i=0; i<ROW; i++) p[i]=p[i+1];
41 p[ROW-1]=t;
42 /* delay 0.1 second */
43 delay(100);
44 }
45 getch();
46 }
執行結果:
(螢幕動畫:三角形捲軸)
【範例 8-5-2】
函數指標的應用 -- qsort
qsort 函數是 TurboC++ 內建的排序函數,使用 quick sort 演算法,可適用多種資料型態的排序,呼叫時必須代入進行比較的函數指標。
1 /* Example 8-5-2 */
2 /* function pointer: qsort() -- quick sort */
3 #include <stdio.h>
4 #include <stdlib.h>
5 #include <string.h>
6
7 int sort_function ( const void *a, const void *b);
8
9 char list[5][4] = { "cat", "car", "cab", "cap", "can" };
10
11 main()
12 {
13 int x;
14
15 printf("before sort:\n");
16 for (x=0; x<5; x++)
17 printf("%s\n",list[x]);
18 qsort((void *)list, 5, sizeof(list[0]), sort_function);
19 printf("after sort:\n");
20 for (x=0; x<5; x++)
21 printf("%s\n",list[x]);
22 }
23
24 int sort_function ( const void *a, const void *b)
25 {
26 return( strcmp((char *)a,(char *)b) );
27 }
執行結果:
before sort:
cat
car
cab
cap
can
after sort:
cab
can
cap
car
cat
第八章 習 題
1. 設計一程式,宣告一整數變數 x 及整數指標 xp,將 xp 指向 x。
(1) 印出 x 的內容與位址。
(2) 印出 xp 所指的內容與位址。
2. 寫出下列程式的輸出。
(1) int data, *p;
data=3;
p=&data;
printf("%d %d",data,*p);
(2) int a=2, b=3;
int *p=&a, *q=&b;
printf("%d",*p+*q);
(3) char c, *cp;
p=&c;
*p='A';
c=c+1;
printf("%c %c",c,*p);
3. 設計一函數,傳入一整數指標,印出所指向整數的內容。
4. 設計一函數,將兩個傳入的浮點數變數內容互換。
5. (1) 宣告一 5 個元素的整數陣列 t 。
(2) 宣告整數指標 tp。
(3) 將 tp 指向 t。
(4) 輸入 t 陣列的內容。
(5) 印出 tp 所指陣列所有元素的內容。
6. 寫出下列程式的輸出。
(1) int a[5]={8,6,7,-3,0};
int *p=&a[0];
printf("%d %d %d %d",a[0],*(p+1),*p+1,*(p+3));
(2) int a[5]={9,7,5,4,3};
int *p=a, i;
for (i=0; i<5; i++)
printf("%d",*(p+i));
(3) int a[5]={1,3,5,7,9};
int *p=a, sum=0, i;
for (i=0; i<5; i++)
sum += *p++;
printf("%d %d",i,sum);
(4) int a[]={11,22,33,44,55,66};
int *p=a;
printf("%d %d",sizeof(a),sizeof(*p));
(5) int a[]={2,4,7,8,10};
int *p=a, i;
for (i=0; i<sizeof(a)/sizeof(int); i++)
if (p[i] %2 == 0)
printf("%d ",p[i]);
(6) char name[10]="Peterson";
char *p=name;
printf("%c %c %c",name[1], *p, *(p+2));
(6) char s[]={'J','o','r','d','o','n','\0'};
char *p=s;
while (*p != '\0') p++;
printf("%d",p-s);
(7) int x[]={1,-1,3,-3,5,-5,0};
int *p=x, i;
i=0;
while (*p++ != 0) i++;
printf("%d",i);
(8) int num[]={1,3,5,7,9,11,13};
int *p=&num[0], s=0;
while (*p != 13) s += *p++;
printf("%d",s);
7. 試說明下列宣告的意義。
(1) int *a;
(2) int *aa;
(3) int *a[5];
(4) int (*a)[5];
8. 設計一程式,宣告 10 個元素的整數陣列,用指標完成下列動作。
(1) 印出陣列內容。
(2) 求出陣列元素的最大值。
(3) 求出陣列元素的平均值。
9. 將習題 8 的各項改成函數版。
10. 設計一程式,用指標將兩個一維整數陣列 a , b 的對應元素相加,設陣列元素個數都是 N,結果存入 c 陣列。
11. 設計一程式,用指標將兩個一維整數陣列 a , b 的內容合併為 c 陣列,設 a 陣列有 N 個元素,b 陣列有 M 個元素。
12. 設計一程式,用指標陣列儲存一月至十二月的英文名稱,將其一一印出。
13. 試說明下列宣告的意義。
(1) int *func();
(2) int (*func)();
(3) int (*func[5])();