2008/03/28

Function Pointer & Callback Function

什麼是 Function Pointer (函式指標)

函式指標是指標的一種,它是用來指到一個函式的位置。

當我們在跑程式時,變數、函式都是放在某些記憶空間裡,我們之前所學的指標(pointer)是用來指到一個變數的位置(variable),例如:

int *a; // a是指向整數的指標

float *b; // b是指向浮點的指標

float (*pt2Func)(int, int); // pt2Func是一個指向函式的指標,而它所指向的函式有二個輸入

// arg. (型態分別為 int, int),以及回傳float的值。

以函式指標來取代Switch/if

以下我們先看一個例子,假設我們有四個可以使用的函式,分別是代表加、減、乘、除:

float Plus (float a, float b) { return a+b; }
float Minus (float a, float b) { return a-b; }
float Multiply(float a, float b) { return a*b; }
float Divide (float a, float b) { return a/b; }

如果我們要依輸入的條件使用這四個函式,則我們通常會用switch/if的方式來寫:


// The four arithmetic operations ... one of these functions is selected
// at runtime with a swicth or a function pointer

float Plus (float a, float b) { return a+b; }
float Minus (float a, float b) { return a-b; }
float Multiply(float a, float b) { return a*b; }
float Divide (float a, float b) { return a/b; }


// Solution with a switch-statement - <opCode> specifies which operation to execute
void Switch(float a, float b, char opCode)
{
float result;

// execute operation
switch(opCode)
{
case '+' : result = Plus (a, b); break;
case '-' : result = Minus (a, b); break;
case '*' : result = Multiply (a, b); break;
case '/' : result = Divide (a, b); break;
}

cout << "Switch: 2+5=" << result << endl; // display result
}

下面我們以"指標函式"的方式來實現上面的程式(switch/if):

// The four arithmetic operations, one of these functions is selected at runtime

float Plus (float a, float b) { return a+b; }
float Minus (float a, float b) { return a-b; }
float Multiply(float a, float b) { return a*b; }
float Divide (float a, float b) { return a/b; }

void Switch_With_Function_Pointer(float a, float b, float (*pt2Func)(float, float))
{
float result = pt2Func(a, b); // call using function pointer

cout << "Switch replaced by function pointer: 2-5="; // display result
cout << result << endl;
}


// Execute example code
void Replace_A_Switch()
{
cout << endl << "Executing function 'Replace_A_Switch'" << endl;

Switch(2, 5, /* '+' specifies function 'Plus' to be executed */ '+');
Switch_With_Function_Pointer(2, 5, /* pointer to function 'Minus' */ &Minus);
}

程式註解如下:

宣告一個函式 Switch_With_Function_Pointer(),這個函式有三個arg.,分別為 a, b, pt2Func,其中a, b都為float的型態,另外pt2Func為函式指標型態,其所指向的函式具有二個輸入arg.(為float, float),以及會回傳值的型態為float。

pt2Func會指到哪個函式呢?如果我們將它指到"函式Minus",則它就會指到減法的運算,以此類推,如果將它指到"函式Divide",則pt2Func會指到減法運算的函式。(注意:Minus, Plus, Multiply, Divide函式為二個輸入arg. (float, float),以及回傳float型態值,這些特徵和float (*pt2Func)(float, float)的特徵一致)

如果pt2Func是指到"函式Divide",則在Switch_With_Function_Pointer{ }裡的字眼"pt2Func"可以以"Divide"來看待,因此,在Switch_With_Function_Pointer{ }裡的float result = pt2Func(a, b);可以看成為float result = Divide(a, b);

接下來,我們看主函式Replace_A_Switch(),在主函式裡,我們可以看到Switch_With_Function_Pointer(2, 5, &Minus);,這段程式是呼叫函式Switch_With_Function_Pointer(),並且將a值設為2、b值設為5,以及pt2Func指㺫Minus函式的位置。值得注意的是&Minus代表「函式Minus()的位置」。

如何宣告函式指標

int (*pt2Function)(float, char, char) = NULL; // C
int (TMyClass::*pt2Member)(float, char, char) = NULL;
// C++, TMyClass為定義的類別 (指到static函式)

程式註解:

宣告一個函式指標pt2Function,這個指標是指到一個函式,且這個函式有三個輸入arg. (float, char, char)。我們亦將pt2Function初始為NULL。

宣告一個函式指標pt2Member,這個指標是指到一個在類別TMyClass裡的函式,且這個函式有三個輸入arg. (float, char, char)。我們亦將pt2Member初始為NULL。

如何指派函式位置給函式指標

// Note: Although you may ommit the address operator on most compilers
// you should always use the correct way in order to write portable code.


// C

int (*pt2Function)(float, char, char) = NULL;


int DoIt (float a, char b, char c){ printf("DoItn"); return a+b+c; }
int DoMore(float a, char b, char c)const{ printf("DoMoren"); return a-b+c; }

pt2Function = DoIt;
// short form
pt2Function = &DoMore; // correct assignment using address operator


程式註解:

在C程式的寫法裡,程式pt2Function = DoIt是指「將函式指標pt2Function指到函式DoIt」,但這樣寫法並不是最標準的寫法,正確的寫法應該為pt2Function = &DoIt

C++程式寫法可以參考如下:

// C++
class TMyClass
{
public:
int DoIt(float a, char b, char c){ cout << "TMyClass::DoIt"<< endl; return a+b+c;};
int DoMore(float a, char b, char c) const
{ cout << "TMyClass::DoMore" << endl; return a-b+c; };

/* more of TMyClass */
};

int (TMyClass::*pt2Member)(float, char, char) = NULL;
pt2Member = &TMyClass::DoIt; // note: correct assignment using address operator
// <pt2Member> may also legally point to &DoMore

程式註解:

在C++程式的寫法裡,我們首先宣告一個類別TMyClass,其包含二個公用函式 DoIt()和DoMore()。再來宣告一個函式指標pt2Member,這個指標是指到TMyClass裡的一個函式,而這個函式有三個輸入arg.,並且回傳int型態值。我們將pt2Member指到類別TMyClass裡的函式DoIt()。

如何用==、!=來比較函式指標

// C
if(pt2Function >0){ // check if initialized
if(pt2Function == &DoIt)
printf("Pointer points to DoItn"); }
else
printf("Pointer not initialized!!n");


// C++
if(pt2ConstMember == &TMyClass::DoMore)
cout << "Pointer points to TMyClass::DoMore" << endl;

如何用函式指標來呼叫函式

以函式指標來取代Switch/if這一節的例子裡,我們已經看過如何用函式指標來呼收函式,在這裡我們稍微整裡如下:

// calling a function using a function pointer
int result1 = pt2Function (12, 'a', 'b');
// C short way
int result2 = (*pt2Function) (12, 'a', 'b');
// C

TMyClass instance1;
int result3 = (instance1.*pt2Member)(12, 'a', 'b');
// C++
int result4 = (*this.*pt2Member)(12, 'a', 'b'); // C++ if this-pointer can be used

TMyClass* instance2 = new TMyClass;
int result4 = (instance2->*pt2Member)(12, 'a', 'b');
// C++, instance2 is a pointer
delete instance2;

程式註解:

在C程式的寫法裡,上述的二種方式都可以(亦即可以忽略 * )。

在C++程式的寫法裡,請自行觀察寫法,值得注意的是 instance1為TMyClass型態的變數,但instance2為TMyClass型態的指標。在類別中,如果是變數則要用.*來存取其成員函式,例如:instance1.pt2Member。反之,在類別中,如果是指標則要用->來存取其成員函式,例如:instance2->pt2Member。

在這裡有個提外話,在結構(structure)裡,我們也可以發現相同的用法: 如果為變數,則用 . 來存取其成員,如果為指標,則用->來存取其成員。

struct MyStruct

{

char Age[10];

int Grade;

};

MyStruct Willy;

MyStruct *Ivy;

Willy.Grade= 80;

(*Ivy).Grade= 100; // (*Ivy)是Ivy指標所指的位置上面的值

Ivy->Grade= 100; // 同 (*Ivy).Grade= 100 用法

如何傳遞函式指標到另一個函式中

在這裡我們提到,如何將函式指標傳遞到另一個函式中,我們舉一個例子如下:

// <pt2Func> is a pointer to a function which returns an int and takes a float and two char
void PassPtr(int (*pt2Func)(float, char, char))
{
int result = (*pt2Func)(12, 'a', 'b'); // call using function pointer
cout << result << endl;
}

// execute example code - 'DoIt' is a suitable function like defined above

void Pass_A_Function_Pointer()
{
cout << endl << "Executing 'Pass_A_Function_Pointer'" << endl;
PassPtr(&DoIt);
}

程式註解:

在函式PassPtr()中有一個輸入的arg.,這個arg.是一個函式指標(其名為pt2Func),這個函式指標是指到一個函式(三個輸入(float, char, char)和void型態的回傳值)。

在主函式Pass_A_Function_Pointer()中,我們呼叫PassPtr()函式,並且把其argument設定為&DoIt,亦即將pt2Func指到DoIt()函式的位置。因此PassPtr()被呼叫後,這段程式int result = (*pt2Func)(12, 'a', 'b');等效上就是執行int result = DoIt(12, 'a', 'b');的意思。

如何讓函式回傳函式指標

在這節我們討論如何讓函式回傳函式指標,我們一樣以一段程式來解說如下:

// Solution using a typedef: Define a pointer to a function which is taking two floats and returns a float
typedef float(*pt2Func)(float, float);

// Function takes a char and returns a function pointer which is defined
// with the typedef above. <opCode> specifies which function to return

pt2Func GetPtr2(const char opCode)
{
if(opCode == '+')
return &Plus;
else
return
&Minus; // default if invalid operator was passed
}


// Execute example code
void Return_A_Function_Pointer()
{
cout << endl << "Executing 'Return_A_Function_Pointer'" << endl;

// define a function pointer and initialize it to NULL
float (*pt2Function)(float, float) = NULL;

pt2Function=GetPtr1('+'); // get function pointer from function 'GetPtr1'
cout << (*pt2Function)(2, 4) << endl; // call function using the pointer


pt2Function=GetPtr2('-'); // get function pointer from function 'GetPtr2'
cout << (*pt2Function)(2, 4) << endl; // call function using the pointer
}

程式註解:

typedef是用來定義型態,程式typedef float(*pt2Func)(float, float);意指定義一個型態pt2Func,這個型態是一個函式指標(二個輸入,回傳float)。這個型態就像我們常見的型態int, int*...一樣,int為整數型態,int*為整數指標的型態。

pt2Func GetPtr2(const char opCode)為一個函式,這個函式有一個輸入arg.(名為opCode,型態為const char),並且會回傳型態pt2Func。在進一步看這個函式的內容,當輸入的opCode=='+' ,則回傳&Plus,意即回傳Plus()函式的位置。

在主函式Return_A_Function_Pointer()中,程式float (*pt2Function)(float, float) = NULL;首先定義一個函式指標pt2Function並且將它初始化為Null。之後程式呼叫函式GetPtr1(),並將其回傳的值設定給pt2Function,例如:pt2Function=GetPtr1('+');是將pt2Function指向函式Plus(),所以其後程式(*pt2Function)(2, 4)是執行 2+4的動作,並回傳值。

下面為另外一個「如何讓函式回傳函式指標」方法(但我覺得不容易理解,建議可以直接跳過)

// Direct solution: Function takes a char and returns a pointer to a
// function which is taking two floats and returns a float. <opCode>
// specifies which function to return

float (*GetPtr1(const char opCode))(float, float)
{
if(opCode == '+')
return &Plus;
else
return
&Minus; // default if invalid operator was passed
}

如何使用函式指標的陣列

有二種方法可以宣告函式指標陣列,一種是使用typedef,另一種是直接宣告,我們以例子說明如下:

// C
// type-definition: 'pt2Function' now can be used as type

typedef int (*pt2Function)(float, char, char);

// illustrate how to work with an array of function pointers
void Array_Of_Function_Pointers()
{
printf("nExecuting 'Array_Of_Function_Pointers'n");

// define arrays and ini each element to NULL, <funcArr1> and <funcArr2> are arrays
// with 10 pointers to functions which return an int and take a float and two char


// first way using the typedef
pt2Function funcArr1[10] = {NULL};

// 2nd way directly defining the array
int (*funcArr2[10])(float, char, char) = {NULL};


// assign the function's address - 'DoIt' and 'DoMore' are suitable functions like defined above

funcArr1[0] = funcArr2[1] = &DoIt;
funcArr1[1] = funcArr2[0] = &DoMore;

/* more assignments */

// calling a function using an index to address the function pointer
printf("%dn", funcArr1[1](12, 'a', 'b')); // short form
printf("%dn", (*funcArr1[0])(12, 'a', 'b')); // "correct" way of calling
printf("%dn", (*funcArr2[1])(56, 'a', 'b'));
printf("%dn", (*funcArr2[0])(34, 'a', 'b'));
}

程式註解:

宣告函式指標陣列的方法有二,一種為先定義型態typedef int (*pt2Function)(float, char, char);,pt2Function是一個函式指標(二個輸入,回傳float)的型態。再來我們就可以宣告一個函式指標陣列,如程式pt2Function funcArr1[10] = {NULL};funcArr1是一個函式指標的陣列。另外一種宣告方法為直接宣告,例如:int (*funcArr2[10])(float, char, char) = {NULL};

因此,我們可以做下列程式的操作:funcArr2[1] = &DoIt;,意即將函式指標funcArr2[1]指向函式DoIt()的位置。

我們也可以呼叫函式,例如:(*funcArr2[1])(56, 'a', 'b');是執行DoIt(56,'a','b')並回傳值的意思。

下面亦提供一個C++程式,做參考(可忽略跳過)

// C++

// type-definition: 'pt2Member' now can be used as type
typedef int (TMyClass::*pt2Member)(float, char, char);

// illustrate how to work with an array of member function pointers
void Array_Of_Member_Function_Pointers()
{
cout << endl << "Executing 'Array_Of_Member_Function_Pointers'" << endl;

// define arrays and ini each element to NULL, <funcArr1> and <funcArr2> are
// arrays with 10 pointers to member functions which return an int and take a float and two char

// use an array of function pointers in C and C++. The first way uses a typedef, the second way

// directly defines the array. It's up to you which way you prefer.


// first way using the typedef
pt2Member funcArr1[10] = {NULL};

// 2nd way of directly defining the array
int (TMyClass::*funcArr2[10])(float, char, char) = {NULL};

// assign the function's address - 'DoIt' and 'DoMore' are suitable member

// functions of class TMyClass like defined above
funcArr1[0] = funcArr2[1] = &TMyClass::DoIt;

funcArr1[1] = funcArr2[0] = &TMyClass::DoMore;
/* more assignments */

// calling a function using an index to address the member function pointer
// note: an instance of TMyClass is needed to call the member functions

TMyClass instance;
cout << (instance.*funcArr1[1])(12, 'a', 'b') << endl;
cout << (instance.*funcArr1[0])(12, 'a', 'b') << endl;
cout << (instance.*funcArr2[1])(34, 'a', 'b') << endl;
cout << (instance.*funcArr2[0])(89, 'a', 'b') << endl;
}

什麼是callback function

callback函式是架構在函式指標的基礎上。在這邊我們用一個qsort()函式的例子說明什麼是callback函式。qsort()是c++標準函式庫裡(cstdlib)的一個內鍵函式,更多關於qsort()的資訊可以在這裡找到:http://www.cplusplus.com/reference/clibrary/cstdlib/qsort.html,我們簡單介紹qsort()函式如下:

void qsort ( void * base, size_t num, size_t size, int ( * comparator ) ( const void *, const void * ) );

void *basebase是一個指標,指向一個型態未知的區域,例如:int array[10]; base=array;,則base是指向一個含有10個elements的指標。size_t num:num是代表區域中有幾個elements,如上例,有10個elements。size_t size:size是代表每個element是幾個bytes。int ( * comparator ) ( const void *, const void * ):comparator是一 個函式指標,指向一個函式,這個函式有二個輸入arg.(未知型態指標, 未知型態指標)並且回傳int型態。函式qsort()的功用是把base所指向區域的elements做排序,而排序規則是根據函式指標comparator所指向函式的定義。我們簡單表示qsort()的程式架構如下:


void qsort( ... , int (*comparator)(const void*, const void*))
{

for( ... )

{

for( ... )

{

...

int bigger=comparator(item1, item2); // make callback of comparing function

// note: item1 and item2 are void-pointers
... // 其他程式....

}

}
}

我們可以發現在標準函式庫裡的qsort()函式會利用函式指標comparator來呼叫其所指向的函式,並且拿這個函式來比較二個項(item1, item2)的大小,這邊我們要強調的是"孰大孰小"是根據函式的內容來定義,所以這樣的行為就稱之為callback。下面我們再舉一個完整的程式來說明qsort()如何使用callback:

// How to make a callback in C by the means of the sort function qsort

#include <stdlib.h> // due to: qsort
#include <time.h> // due to: randomize
#include <stdio.h> // due to: printf

// comparison-function for the sort-algorithm; two items are taken by void-pointer, converted and compared
int CmpFunc(const void* _a, const void* _b)
{
// you've got to explicitly cast to the correct type; 做強制形態轉換
const float* a = (const float*) _a;
const float* b = (const float*) _b;

if(*a > *b) return 1; // first item is bigger than the second one -> return 1
else
if
(*a == *b) return 0; // equality -> return 0
else return -1; // second item is bigger than the first one -> return -1

}


// example for the use of qsort()
void QSortExample()
{
float field[100];

::randomize(); // initialize random-number-generator
for(int c=0;c<100;c++) // randomize all elements of the field
field[c]=random(99);

// sort using qsort()
qsort((void*) field, 100, sizeof(field[0]), CmpFunc);

// display first ten elements of the sorted field
printf("The first ten elements of the sorted field are ...n");
for(int c=0;c<10;c++)
printf("element #%d contains %.0fn", c+1, field[c]);
printf("n");

}

程式註解:

首先我們先定義一個函式int CmpFunc(const void* _a, const void* _b),這個函式有兩個輸入arg.(const void* _a, const void* _b)並回傳int型態值,我們要再強調的是:「函式所定義的輸入和回傳的型態,要與函式指標所指向的函式型態相符」。在CmpFunc()函式中,首先會將未定義型態的輸入arg. _a_b做強置型態轉換到float型態的ab,如程式:const float* a = (const float*) _a;const float* b = (const float*) _b;。接下來的程式會比較ab的大小並且回傳結果,如程式:


if(*a > *b) return 1;

else
if
(*a == *b) return 0;

else return -1;

在主程式QSortExample()中,我們先宣告一個陣列field[100],並且將field陣列填滿隨機值。之後,我們使用qsort()函式來排序field陣列裡的值,如程式:qsort((void*) field, 100, sizeof(field[0]), CmpFunc);在這程式中,因為qsort()的第一個輸入arg.要為void*型態,所以以(void*) field將field由(float*)做強制型置轉換到(void*)型態,另外,qsort()的第四個arg.則設定為CmpFunc,代表函式指標將指向CmpFunc()函式的位置(注意:&CmpFunc為標準的寫法,但CmpFunc的寫法大部分編譯器也ok)。