2017年4月11日 星期二

第八章 指標



                     第八章   指標

   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 就是位 1002p+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]
    如果談得詳細一點,陣列和 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 列第 行的元素為 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 個元素,陣列有 M 個元素。

12. 設計一程式,用指標陣列儲存一月至十二月的英文名稱,將其一一印出。

13. 試說明下列宣告的意義。
    (1) int *func();
    (2) int (*func)();
    (3) int (*func[5])();

沒有留言:

張貼留言