星期日, 11月 09, 2014

C pointer to function 函式指標 學習心得



這是函式指標學習心得的第一篇,在這一篇中你會學習到:

1. 函式在記憶體中的情況
2. 如何宣告一個函式指標
3. 函式的宣告
4. 使用 typedef 來定義一個函式指標的類型

本篇著重在介紹函式指標,實際應用方面會在下一篇介紹

------------------------ 1. 
函式在記憶體中的情況 ------------------------

在真正的開始學習函式指標 ( pointer to function ) 之前,我們要先弄懂到底什麼是函式以及函式在記憶體分布的情形。

當一個程式碼檔案 ( .c , .cpp ...) 被執行的時候,會產生出一個程序 ( process ),此時才會開始佔用記憶體及進行各種運算。先前我們定義的指標都是針對一個資料型態,如:int * 這是一個指向 int 資料類型的指標。然而,C為什麼沒有為 function 也定義出一個資料型別?因為不同的 function 依照建立的方式不同而有不同的 type ,所以沒辦法替每一種 function 都定義出一個通用的型別。

實際上,function 和一般的資料儲存的狀況不太一樣,他儲存的是一堆的指令。
我們可以參考下面的圖片: 
圖片來源:http://ccckmit.wikidot.com/cp%3Aenvironment


當一個 process 產生的時候,會有一塊自己可以使用的記憶體,並且這個記憶體分成許多不同的區塊,儲存不同的東西。一般來說,我們使用的區域變數是儲存在 stack 區段。程式碼則是儲存在 .text 區。因此,雖然和一般的資料型式不同,但是程式碼也是會佔用記憶體的!理所當然,他也會有自己的位址。

我們可以利用反組譯的方式來查看:

編譯方式:gcc -g -o test test.c

在這裡我使用 gdb 來輔助 
第 1 行 顯示 function name 的位址,接著我們到第15行可以發現,function 第 1 行指令的位址和function name的位址是一樣的,所以我們可以得知:其實function name的位址就是function第一行指令的位址。接著,在第 8 行中,我們可以看到main呼叫函式也是利用函式第 1 行指令的位址。

由此可知,其實在function在記憶體中也是有特定儲存的位址。
如果我們在linux的環境中使用 objdump 這個指令可以看得更清楚,他會指名在.text區
Disassembly of section .text:
        .....
        .....
000000000040052d <say_hello>:


------------------------ 2. 如何宣告一個函式指標 ------------------------

宣告方式: return_type (*func_pointer)( parameter list );
例如: 

第8行宣告一個指標名為 fptr ,指向 int (int, int)
換句話說,fptr的type : int (*)(int, int)

我們可以用以下兩種方式將已知的function, assign 給 function pointer:

1. fptr = &func_name;
2. fptr = func_name;

使用的時候也有兩種方法可以使用:

1. (*fptr)(num1, num2);
2. fptr(num1, num2);

每次讀到這一段的時候,總是有個疑惑:為什麼兩種方法都可以?
甚至,你編譯下面這一段 code 也可以執行:

Why? 

要確保初值化(initialization)或是assignment的正確性,取決於 1.數值 2.型別 
舉例來說:
int func(int, int);
int (*fptr)(int, int);

type of fptr is : int (*)(int, int);
type of func is : int (int, int);
type if &func is : int (*)(int , int);

所以 fptr = &func 很合理,型別正確並且數值也正確(&func數值和func一樣)
可是 fptr = func 型別不一樣。 所以在這裡其實做了implicit conversion 
將function name ( i.e. function designator )轉成函式指標使用

至於,使用的方式有兩種一種用deference(i.e use * operator),一種不用
我曾經在一本書上看到這樣的解釋:

在使用function的時候,fun(),其中()稱作 function-call operator
function-call operator 只允許 pointer to function使用。

所以一般我們在使用func_name(); 其實會做implicit conversion將func_name轉型
所以我們這樣寫其實也可以執行(&func_name)();

以上就是 function pointer 的基本操作。

------------------------ 3. 函式的宣告 ------------------------

不知道大家有沒有想過一個問題:

我們在宣告變數的時候,都是依循這樣的形式 : type var_name;
為什麼宣告function是 : return_type func_name( parameter list );

其實我們的看法應該是這樣 func_name( parameter list) 這一整個和var_name對照
所以,我們在gdb中檢查 func_name( parameter list) 這整個東西的type會和return type是一樣的。

在gdb中要查看變數的type可以使用:ptype var_name / whatis var_name 

利用先前的例子:         
(gdb) ptype say_hello  
type = void ()         
(gdb) ptype say_hello()
type = void            
(gdb) ptype &say_hello 
type = void (*)()      


------------------- 4. 使用 typedef 來定義一個函式指標的類型 ---------------

每次我們要宣告一個 function pointer 假如都要照之前那樣寫,對大多數人來說其實不太容易看。更甚者,牽扯到一堆轉型的時候更讓人頭暈目眩。

因此我們利用typedef來定義一個function pointer的type

int (*ptr)(int, int);  // declare a pointer to function : ptr
                       // and its type is : int (*)(int, int)

typedef int (*func_t)(int, int);
這個時候,func_t 就是一個 int(*)(int, int)的型別了

這個語法可能讓人感到confuse,因為以前我們定義的方式很單純:
typedef int bool;
在這裡我會這樣看 (*func_t)(int, int)是一個東西,然後藉由
typedef int (*func_t)(int, int);來間接定義func_t

所以,以後我們就可以這樣寫了:

謝謝觀看,如果有寫的不清楚或是有謬誤的地方還請各界先賢不吝賜教
感謝!


reference : 
1. http://www.newty.de/fpt/index.html
2. linux C 一站式編程
3. google 大神








[Paper note] Multiagent Bidirectionally-Coordinated Nets for Learning to Play StarCraft Combat Games

Paper:  https://arxiv.org/abs/1703.10069 這篇論文是由 Alibaba & UCL共同發表的,並投稿到 NIPS 2017。他們發表了一個 Multi-agent framework 讓 agents 利用共同的 framew...