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 區。因此,雖然和一般的資料型式不同,但是程式碼也是會佔用記憶體的!理所當然,他也會有自己的位址。

我們可以利用反組譯的方式來查看:
#include<stdio.h>
void say_hello();
void say_hello(){
printf("Hello\n");
return;
}
int main(){
say_hello();
return 0;
}
view raw test.c hosted with ❤ by GitHub

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

在這裡我使用 gdb 來輔助 
(gdb) print say_hello
$1 = {void ()} 0x40052d <say_hello>
(gdb) disassemble main
Dump of assembler code for function main:
0x000000000040053e <+0>: push %rbp
0x000000000040053f <+1>: mov %rsp,%rbp
0x0000000000400542 <+4>: mov $0x0,%eax
0x0000000000400547 <+9>: callq 0x40052d <say_hello>
0x000000000040054c <+14>: mov $0x0,%eax
0x0000000000400551 <+19>: pop %rbp
0x0000000000400552 <+20>: retq
End of assembler dump.
(gdb) disassemble say_hello
Dump of assembler code for function say_hello:
0x000000000040052d <+0>: push %rbp
0x000000000040052e <+1>: mov %rsp,%rbp
0x0000000000400531 <+4>: mov $0x4005e4,%edi
0x0000000000400536 <+9>: callq 0x400410 <puts@plt>
0x000000000040053b <+14>: nop
0x000000000040053c <+15>: pop %rbp
0x000000000040053d <+16>: retq
End of assembler dump.
view raw gistfile1.txt hosted with ❤ by GitHub
第 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 );
例如: 
#include<stdio.h>
int add(int n1, int n2){
return n1 + n2;
}
int main(){
int (*fptr)(int, int);
fptr = add;
int num1 = 10, num2 = 30;
printf("num1 + num2 is : %d\n", fptr(num1, num2));
return 0;
}
view raw fp.c hosted with ❤ by GitHub

第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 也可以執行:

#include<stdio.h>
void print_hello(){
printf("Hello world\n");
return;
}
int main(){
print_hello();
(&*print_hello)();
(*&*print_hello)();
(**&print_hello)();
(&**&print_hello)();
(&*&*print_hello)();
return 0;
}
view raw test.c hosted with ❤ by GitHub
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 大神








這個網誌中的熱門文章

Vim 自動 補完 插件 YouCompleteMe 教學 心得 新手入門 C++ C

怎麼建立一個 Https server?