Lab 3-2: 函式指標 (Function Pointer)

   
實習目標

1. 瞭解什麼是 C/C++ 中的函式指標 (Function pointer)

2. 為什麼需要函式指標這種語法

3. 使用 C/C++ 中函式指標 (Function pointer) 的語法

4. 多型的概念

   
說明一

什麼是函式指標?

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

  const 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 架構的計算機硬體,如果你會用一種語言來寫,基本上就應該能夠用其它語言寫,只是格式和名稱的轉換而已,就算以後再有新的語言出現,也脫不出這個範疇,這也是一個抽象化的範例。

以下面程式片段來說:

int data[5]={5,3,9,2,8}, ndata=sizeof(data)/sizeof(int);
bubblesort(data, ndata);
bubblesort(data, ndata); 固定呼叫特定的 bubblesort 函式, 可是如果改成
int data[5]={5,3,9,2,8}, ndata=sizeof(data)/sizeof(int);
void (*fp)(int [], int);
...
(*fp)(data, ndata);
(*fp)(data, ndata); 呼叫的可以是任意一個 sort 函式, 要看 fp 設定為哪一個函式的指標, 如果在呼叫之前有一列 fp = bubblesort; 那麼 (*fp)(data, ndata); 就呼叫到 bubblesort, 如果在呼叫之前有一列 fp = selectionsort; 那麼 (*fp)(data, ndata); 就呼叫到 selectionsort, 所以(*fp)(data, ndata); 就變成是一般性的動作了, 程式可以依據不同的需求來使用不同的排序演算法, 但是上面那個資料陣列 data 在執行完 (*fp)(data, ndata) 之後就是會依照順序排列
說明三

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

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

範例執行程式

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

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

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

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

  1. size 欄位打算放的是整個資料結構的大小 (單位是位元組),
  2. print 欄位為函式指標變數,打算放各個列印不同資料的函式位址,
  3. data 陣列欄位基本上只是代表結構裡所有其它資料的起始位址而已,陣列的元素個數設為 1 並不代表只有一個位元組,只是各種資料的大小不一樣,不管寫多少都無法滿足所有的需求 (有些編譯器允許 unsigned char data[0] 的寫法)
我們並不打算透過這樣子的 SaleItem 結構來配置記憶體存放資料,我們只是打算用這樣的資料型態的指標泛指所有不同商品的資料結構
步驟五 接下來我們定義存放個別商品資訊的資料結構 (Book, Magzine, 與 VCD),並且撰寫列印不同商品的列印程式 (printBook(struct SaleItem *item), printMagzine(struct SaleItem *item), 與 printVCD(struct SaleItem *item)),例如:
  struct Book
  {
    int size;
    void (*print)(struct 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)(struct 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)(struct 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++ 中有很方便的語法可以實作這種多型。

步驟八 請助教檢查後, 將所完成的 專案 (只需保留 .cpp, .h, .sln 以及 .vcxproj 檔案即可; 刪除掉 .suo, .sdf, .filters, .users, debug\ 資料匣, 以及 ipch\ 資料匣下的所有內容) 壓縮起來, 選擇 Lab3-2 上傳, 後面的實習課程可能需要使用這裡所完成的程式
  在 C++ 語法中還有另外一種函式指標, 就是成員函式的指標, 和上面所談到的函式指標型態不同, 不能夠混用, 以下面的範例來說明:
class IntStack {
public:
    void push(int x);
    int pop();
private:
    ...
} mystack, *ptr=&mystack;
成員函式指標 fptr 定義如下:
void (IntStack::*fptr)(int);
使用時先設定內容
fptr = &IntStack::push; // 一定要有取位址符號 &
透過此指標呼叫函式的方法只有下列兩種:
(mystack.*fptr)(10);
((&mystack)->*fptr)(10); // (ptr->*fptr)(10);
效果和
mystack.push(10);
是一樣的。

如果是樣版類別的成員函式, 語法也是類似的

#include <vector>
#include <iostream>
#include <iomanip>
using namespace std;

void main()
{
    int i, x[] = {1,2,3,4};
    vector<int> v(x, x+4);
    vector<int>::size_type (vector<int>::*fptr)(void) const;
    fptr = &vector<int>::size;
    cout << &vector<int>::size << " " << fptr<< endl;
    cout << v.size() << " " << (v.*fptr)() << endl;
}

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

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