2018年4月1日 星期日

函式傳遞的進化論 (1) - function pointer

在C語言中,可以使用function pointer (函式指標, 一個特殊的指標用來指向函式) 將一個函式當作參數傳給另一個函式。

#include <stdio.h>

int add (int firstNumber, int secondNumber);
int subtract (int firstNumber, int secondNumber);
int calculator (int firstNumber, int secondNumber, int (*fp) (int, int));
int main ()
{
  int firstNumber = 1;
  int secondNumber = 2;
  int (*fp) (int, int) = add;
  printf ("%d\n", calculator (firstNumber, secondNumber, fp));
  
  fp = subtract;
  printf ("%d\n", calculator (firstNumber, secondNumber, fp));

  return 0;
}

int add (int firstNumber, int secondNumber)
{
  return firstNumber + secondNumber;
}

int subtract (int firstNumber, int secondNumber)
{
  return firstNumber - secondNumber;
}

int calculator (int firstNumber, int secondNumber, int (*fp) (int, int))
{
  return (*fp) (firstNumber, secondNumber);
}

程式碼如上。宣告2個函式add & subtract以及1個函式calculator。
其中calculator的參數允許傳入一個函式來讓calculator呼叫該函式。
在第10行時同樣宣告一個函式指標fp,並且指向add函式。
第11行時則將函式指標fp作為引數傳遞給calculator函式。因為此時fp指向add函式,因此claculator函式會呼叫add函式。
在13行時則將函式指標fp改成指向subtract函式,因此在14行的calculator會呼叫subtract函式。

以上是教科書講到函式指標時經常舉的例子。
我還在念書時對這段程式常有疑惑:
printf ("%d\n", add(firstNumber, secondNumber));
printf ("%d\n", subtract(firstNumber, secondNumber));
像這樣直接呼叫add & subtract函式不就好了,為什麼還要多宣告一個函式指標來指向函式?讓函式指標呼叫該函式,這不是多此一舉嗎?



上面的程式只是為了展示函式指標的語法與使用方法而已,其實函式指標的威力是在實現callback函式的時候。

讓我們再舉個常見的範例-氣泡排序法。
現在我們需要讓一個int陣列排序,不過有時候我們需要能以升序排序,但有時候又需要能以降序排序。
為了實現這個目的,我們必須寫出兩個氣泡排序的程式:bubbleSortByAsc & bubbleSortByDesc

void bubbleSortByAsc(int array[], int length ){
    int round=0, i=0;
    int tmp=0;
    for(round=0; round<length; round++){
        for(i=0; i<length-1-round; i++){
            if(array[i] > array[i+1]){
                tmp = array[i];
                array[i] = array[i+1];
                array[i+1] = tmp;
            }
        }
    }
}
void bubbleSortByDesc(int array[], int length){
    int round=0, i=0;
    int tmp=0;
    for(round=0; round<length; round++){
        for(i=0; i<length-1-round; i++){
            if(array[i] < array[i+1]){
                tmp = array[i];
                array[i] = array[i+1];
                array[i+1] = tmp;
            }
        }
    }
}

你會注意到這兩個函式幾乎全部相同,唯一不一樣的是在第6行的 > 與 < 符號而已。

讓我們回到氣泡排序法的演算法:
1. 尋訪陣列中的所有元素
2. 尋訪時,每次從陣列中取相鄰的兩個元素(array[i]、array[i+1]),決定是否需要交換兩元素
3. 每次尋訪完陣列後可以得到一個最大(or最小)的一個數字
4. 重複1-3步驟

重新看一次演算法以及程式碼,不難發現決定升序或是降序的關鍵點在於"決定是否需要交換兩元素"的部分。
既然演算法的主體都一樣,唯一的差別是在"決定是否要交換兩元素",
那麼我們可不可以把這一部分從氣泡排序法抽離出來,讓使用氣泡排序法的人自己提供"決定是否交換兩元素"的函式呢?

答案當然是YES

要達成這個目的就是前面提到的函式指標,現在氣泡排序法的程式碼會變成這樣:
void bubbleSort(int array[], int length, int (*cmp)(int, int) ){
    int round=0, i=0;
    int tmp=0;
    for(round=0; round<length; round++){
        for(i=0; i<length-1-round; i++){
            if(cmp(array[i], array[i+1]) > 0){
                tmp = array[i];
                array[i] = array[i+1];
                array[i+1] = tmp;
            }
        }
    }
}

我們多傳了一個函式指標進來,在第6行時呼叫函式指標所指向的函式來決定是否需要交換陣列元素。
所以這時候我們想要升序排序時需要撰寫sortByAsc函式:
int sortByAsc(int first, int second){
    if(first < second){
        return -1;
    }
    else if(first > second){
        return 1;
    }
    else{
        return 0;
    }
}

使用時將sortByAsc傳遞給bubbleSort即可。
int main()
{
    int array[] = {3, 19, 6, 4, 20};
    int length = sizeof(array)/sizeof(array[0]);
    
    bubbleSort(array, length, sortByAsc);
    
    int i=0;
    for(i=0; i<length; i++){
        printf("%d\n", array[i]);
    }

    return 0;
}

同樣地,想要降序排序就是寫一個sortByDesc即可。
int sortByDesc(int first, int second){
    return sortByAsc(first, second) * -1;
}

int main()
{
    int array[] = {3, 19, 6, 4, 20};
    int length = sizeof(array)/sizeof(array[0]);
    
    bubbleSort(array, length, sortByDesc);
    
    int i=0;
    for(i=0; i<length; i++){
        printf("%d\n", array[i]);
    }

    return 0;
}

其實這邊的sortByAsc以及sortByDesc函式就是所謂的callback函式。

使用函式指標與callback函式可以讓你的程式更加的精簡與具有彈性。
在現代軟體開發往往都是多個人一起共同開發,一個人負責一小部分。
可能氣泡排序法是由你寫的,但是使用排序法的可能是其他人。
想想原本的寫法,首先你必須要知道使用的人需要多少種排序方法,然後我們就要寫多少種幾乎一模一樣的氣泡排序法。
而使用函式指標後我們只要寫好一種氣泡排序法就好了。

況且這兩種方法的最大差別在於面對未來的需求:
現在我們需要一種新的排序方法:使用升序排序,但是奇數數字排前面,偶數數字排後面。
原本的寫法你必須重新寫一份排序方法、編譯、然後請其他人更新檔案、重新連結後才能使用,
但是callback版本時你完全不需要動工,因為我們已經把決定排序方法的這件事丟給使用者決定了。


題外話:
C提供的標準函式庫裡面有qsort可以使用。
撇除掉qsort為了能支援所有資料形態而使用void*指標外,其設計核心也是使用函式指標。

沒有留言:

張貼留言