Lab 3-2: Function Pointer

 
實習目標

1. 瞭解什麼是 C/C++ 中的函式指標 (Function pointer)
2. 為什麼需要函式指標這種語法
3. 使用 C/C++ 中函式指標 (Function pointer) 的語法
4. 多型的概念

 
說明一

什麼是函式指標?

在 C 程式中資料存放在記憶體中,因此任何一筆資料都有它的起始記憶體位址,不論是字串常數、自動配置的變數 / 陣列、或是動態配置的變數都一樣,例如:

  char *addr1 = "Constant String";
  int *addr2 = &x;  /* int x; */
  double *addr3 = y;  /* double y[100]; */
  char *addr4 = (char *) malloc(sizeof(char)*200);

上述四個變數都是用來存放記憶體位址的變數,你可以用

  printf("%p", addr);

來看到變數裡放的的實際位址數值。

在 von Neuman 計算機架構中,可以執行的程式碼 (例如函式),當然也需要放在記憶體中才能夠執行,因此函式也會有記憶體位址,它基本上是函式載入記憶體後的起始位址,例如:

  #include <cstdio>
	
  double square(double x)
  {
    return x * x;
  }

  void main()
  {
    printf("address of square = %p(%p)\n", square, &square);
    printf("address of main = %p(%p)\n", main, &main);
  }

在上面這個程式中,不論是 square 或是 &square 都沒有呼叫 square 函式, 這樣的寫法只是代表 square 函式的起始位址的常數而已,如果要執行函式呼叫的話,必須寫成:

	square(x);
所謂的函式指標變數就是可以存放函式起始位址的變數,例如,我們定義一個可以存放上述 square 函式位址的指標變數 fp,其使用方法如下:
  double (*fp)(double);
  ...
  fp = square;  // 設定變數內容
  ...
  (*fp)(x); // 呼叫 square() 函式

函式指標變數的宣告必須包含函式的回傳值型態、參數個數、與參數的型態,如此在透過這個指標變數進行函式呼叫時,編譯器才知道如何處理其參數及回傳值。

函式指標變數 fp 的宣告看起來有點不像是個變數,你可以用 typedef 敘述來簡化上面的宣告

typedef double (*FuncPtr_t)(double);
FuncPtr_t fp; // 比較像是一個變數了吧
...
fp = square;
...
(*fp)(x);

函式指標變數的宣告可以非常複雜,以後有機會再詳細解釋。

說明二

我們在第一節課堂中有談到抽象化 (abstraction) 的概念,基本上抽象化是我們簡化事情處理方法的要訣。

例如開車,不管是開什麼樣的車,都有前進 / 後退 / 停止 / 轉向 的基本控制功能,當你不去看各種車子的細部差異而只專注於上面這幾種基本控制功能時,開車就很簡單了。

另一個例子是寫程序化的程式:我們知道可以選擇不同的程式語言來撰寫程序化的程式,例如可以用組合語言、BASIC、FORTRAN、C、PASCAL、COBEL、Perl、PHP 等等,那麼會用很多種語言寫程式的人很了不起囉!? 其實好像不然,程序化的語言基本上都只是在描述如何控制 CPU, Memory, I/O 這三大組件而已,所以除了動作的高階 / 低階之外,其實差別不大,主要只有三大類敘述:資料移轉 (Data Transfer), 流程控制 (Control Flow Transfer), 和算術邏輯運算 (Arithmetic / Logic Operations)。 

運用這三大類基本敘述,可以控制底層 von Neuman 架構的計算機硬體,如果你會用一種語言來寫,基本上就應該能夠用其它語言寫,只是格式和名稱的轉換而已,就算以後再有新的語言出現,也脫不出這個範疇,這也是一個抽象化的範例。

說明三

寫程式時我們常常需要運用其它模組提供的功能來完成指定的功能,下圖中顯示在一個綜合書店中,一個資料列印的模組,需要使用書籍、雜誌、VCD 等等的資料模組來完成列印不同資料的功能,呼叫端模組(資料列印)如果能夠把其它模組提供的功能抽象化(用共通的概念去描述),那麼該程式一定也會簡化一點,擴充程式功能來列印其它種資料的時候也能夠順利一點。

現在讓我們逐步運用 C/C++ 語言中的函式指標來完成這個程式

範例執行程式

步驟四 首先我們知道不同商品所需要記錄的資料欄位與資料格式是不同的,這當然也表示資料列印的模組中列印不同商品的方法也不一樣。

為了作適當的抽象化,我們先定義一種一般化的資料結構如下:

  struct SaleItem
  {
    int size;
    void (*print)(SaleItem *);
    unsigned char data[1];
  };

這個 SaleItem 結構其實只是各種不同商品的資料結構的共同部份,其中

  1. size 欄位打算放的是整個資料結構的大小 (單位是位元組),
  2. print 欄位為函式指標變數,打算放各個列印不同資料的函式位址,
  3. data 陣列基本上只是代表結構裡所有其它資料的起始位址而已,陣列的元素個數設為 1 並沒有什麼特別的意義
我們並不打算透過這樣子的 SaleItem 結構來配置記憶體存放資料,我們只是打算用這樣的資料型態的指標泛指所有不同商品的資料結構
步驟五 接下來我們定義存放個別商品資訊的資料結構 (Book, Magzine, 與 VCD),並且撰寫各個列印程式 (printBook(struct SaleItem *item), printMagzine(struct SaleItem *item), 與 printVCD(struct SaleItem *item)),例如:
  struct Book
  {
    int size;
    void (*print)(SaleItem *);
    char title[80];
    char author[30];
    char publisher[30];
    char year[5];
    double price;
  };
  
  void printBook(struct SaleItem *book)
  {
    struct Book *bPtr = (struct Book *)book;
  
    printf("Book name is %s\n", bPtr->title);
    printf("     author is %s\n", bPtr->author);
    printf("     publisher is %s\n", bPtr->publisher);
    printf("     price is %f\n\n", bPtr->price);
  }
  
  struct Magazine
  {
    int size;
    void (*print)(SaleItem *);
    char title[80];
    char issue[10];
    char year[5];
    char month[3];
    char publisher[30];
    double price;
  };
  
  void printMagazine(struct SaleItem *magazine)
  {
    struct Magazine *mPtr = (struct Magazine *)magazine;

    printf("Magazine name is %s\n", mPtr->title);
    printf("         issue is %s\n", mPtr->issue);
    printf("         month/year is %s/%s\n", 
            mPtr->month, mPtr->year);
    printf("         publisher is %s\n", mPtr->publisher);
    printf("         price is %f\n\n", mPtr->price);
  }
  
  struct VCD
  {
    int size;
    void (*print)(SaleItem *);
    char title[80];
    char seriesTitle[80];
    char casts[60];
    char year[5];
    char producer[30];
    double price;
  };

請參考上述程式範例以及範例執行程式的輸出,使用 stdio 函式庫的 printf() 函式製作 printVCD() 函式的內容。

步驟六 接下來我們要運用上一步驟定義的 struct Book, struct Magzine, 和 struct VCD 資料結構來配置記憶體存放各種不同的商品,並且設定它們的內容:
  void initialize(int *nItems, struct SaleItem *items[])
  {
    struct Book *bPtr;
    struct Magazine*mPtr;
    struct VCD *vPtr;
  
    bPtr = (struct Book*) malloc(sizeof(struct Book)); // 配置
    items[0] = (struct SaleItem *) bPtr; // 強制型態轉換
    bPtr->size = sizeof(struct Book);  // 結構佔記憶體大小
    bPtr->print = printBook;  // 指向列印本結構之函式
    strcpy(bPtr->title, 
	    "Harry Potter and the Prisoner of Azkaban ");
    strcpy(bPtr->author, "J.K. Rowling ");
    strcpy(bPtr->publisher, "Bloomsbury");
    strcpy(bPtr->year, "2000");
    bPtr->price = 7.99;

    mPtr = (struct Magazine*) 
        malloc(sizeof(struct Magazine));
    items[1] = (struct SaleItem *) mPtr;
    mPtr->size = sizeof(struct Magazine);
    mPtr->print = printMagazine;
    strcpy(mPtr->title, "Reader's Digest");
    strcpy(mPtr->issue, "---");
    strcpy(mPtr->year, "2005");
    strcpy(mPtr->month, "03");
    strcpy(mPtr->publisher, 
	    "The Reader's Digest Association, Inc.");
    mPtr->price = 13.5;

    vPtr = (struct VCD*) malloc(sizeof(struct VCD));
    items[2] = (struct SaleItem *) vPtr;
    vPtr->print = printVCD;
    strcpy(vPtr->title, "The Two Towers");
    strcpy(vPtr->seriesTitle, "Lord of the Rings");
    strcpy(vPtr->casts, "Elijah Wood, Ian Mackellen");
    strcpy(vPtr->year, "2003");
    strcpy(vPtr->producer, "Entertainment in Video");
    vPtr->price = 70;

    *nItems = 3;
  }

請注意上面程式中存放這三種商品的資料結構是不一樣的,但是我們嘗試把它們抽象化只看它們相同的部份,所以我們將指向各個資料結構的指標轉換為指向通用資料結構 struct SaleItem 的指標。請注意強制的指標型態轉換其實並沒有修改指標變數的內容,所存放的記憶體位置並沒有改變。

另外在這個範例裡我們只簡單地設定三個商品的內容,以便測試程式的功能。

步驟七 接下來我們來撰寫主程式以及資料列印的程式碼,其架構如下:
  void main()
  {
    int nItems;
    struct SaleItem *items[100];
    int i;
    
    initialize(&nItems, items);
 
  // 請在此處撰寫一個迴圈來列印所有 nItems 個商品
  // 的內容,這個迴圈的內容應該只有一個簡單的函式
  // 呼叫,通通透過欄位 print 這個函式指標變數來呼叫,
  // 因為此時已經將所有商品抽象化看成是一致的 SaleItem 
  // 的東西了

    for (i=0; i<nItems; i++)
      free(items[i]); // 請注意此處也不需要區分資料的種類

    printf("Press enter to continue...");
    getchar();
  }

Hint:

for (i=0; i<nItems; i++)
   (*items[i]->print)(items[i]);

試看看是不是已經達成範例執行程式的功能了呢? 你覺得這個 main() 函式的內容有因為抽象化而簡化嗎? 萬一以後還要增加其它種類的商品時,你覺得這個 main() 函式需要修改嗎? 這是很重要的多型概念, 後續我們在 C++ 中有很方便的語法可以實作這種多型。

步驟八 請助教檢查後, 將所完成的 project (去掉 debug/ 資料匣下的所有內容) 壓縮起來, 選擇 Lab3-2 上傳, 後面的實習課程可能需要使用這裡所完成的程式

C++ 物件導向程式設計課程 首頁

製作日期: 03/07/2005 by 丁培毅 (Pei-yih Ting)
E-mail: pyting@mail.ntou.edu.tw TEL: 02 24622192x6615
海洋大學 電機資訊學院 資訊工程系 Lagoon