函數指標 Function Pointer
在 指標 Pointer 那一章中,我們已經了解到指標是用來儲存一個變數的地址。這章我們要來了解什麼是函數指標。
實際上函數指標也是用來儲存地址,唯一的差別在於它是用來儲存函數的地址,而非變數。
什麼是函數指標
開頭講到,函數指標是用來儲存函數的地址。沒錯,函數跟變數一樣也有地址!
這是因為程式若在執行時遇到函數,它會跳到函數所在的地址,執行在其中的程式碼,之後再跳回一開始跳走的地方。
那麼我們可以如何知道函數的地址呢?我們可以這樣做:
int func() { return 3; } int main() { // 第一種方法 std::cout << "函數的地址:" << reinterpret_cast<void*>(func) << std::endl; // 第二種方法 std::cout << "函數的地址:" << func << std::endl; return 0; }
我們應該會看到類似這樣的輸出
函數的地址:0x401186
這邊可以看到有兩種方法可以得到函數的地址,大部分是第一種。但有些編譯器聰明地知道我們是想要印出函數的地址,所以才有第二種。
為什麼需要函數指標
了解函數指標是什麼之後,另一個重要的問題來了:為什麼我們需要有函數指標?
一個非常常見的函數指標應用是回調函數 Callback Function。回調函數讓一個函數可以在執行時,根據收到的不同函數地址而呼叫相對應的函數。
另一個用途則是可以客製化函數。在某些函數中,可能有些特定邏輯並不是固定的。我們可以針對那些區域自行撰寫新的邏輯。一個非常好的例子就是 C++ 中的 sort() 函數。
函數指標還有其他的功能像是 virtual function 這種進階的應用。
我知道現在你可能都聽不太懂這些在幹嘛,沒關係,接下來我都會慢慢介紹的!
函數指標長怎麼樣
了解了函數指標是什麼後,我們終於要來看看函數指標到底長怎樣了!
一個非常非常簡單的函數指標長這樣:
int (*funcPtr)();
funcPtr
就是一個函數指標,這個函數指標指向了一個函數,而這個被指到的函數並沒有任何參數,而且會回傳一個整數 int
。
因為 funcPtr
是一個一般指標,所以它可以隨時變更所指向的函數,只要函數擁有相同的參數以及回傳型態即可!
我們來看另一個函數指標:
int (*funcPtr)(int, float);
這個函數指標和前一個有相同的概念,唯一的差別在於這個被指到的函數擁有兩個參數,一個是整數 int
,另一個則是浮點數 float
。
實際應用
接下來我們來看看函數指標的實際應用,我們會分為這幾點討論:
- 賦予函數指標實際函數
- 利用函數指標呼叫函數
- 回調函數 Callback function
賦予函數指標實際函數
前一節我們知道了函數指標長怎樣,但我們要怎麼賦予它一個實際的函數呢?
我們可以這樣做:
int func1() int func2() int main() { // funcPtr 指向 func1 int (*funcPtr)(){ &func1 }; // funcPtr 指向 func2 fcnPtr = &func2; return 0; }
可以看到我們有兩種方式,第一種是在創建 funcPtr
時就賦予一個函數,第二種是在之後利用 &
的取址符號取得函數地址並進行賦予。
我們來看看一些可能會犯的錯誤:
int func1(); int func2(int); int main() { // 回傳型態不同 float (*funcPtr)(){ &func1 }; // 參數不相同 int (*funcPtr)(){ &func2 }; return 0; }
參數和回傳型態的不同都會造成程式報錯。
利用函數指標呼叫函數
被賦予了實際函數後,我們要如何呼叫該函數呢?
int func(int x) { return x; } int main() { int (*funcPtr)(int){ &func }; (*funcPtr)(5); return 0; }
和普通的指標擁有一樣概念,我們要使用 *
進行取值,之後再將參數傳進就好啦!其實和一般的函數呼叫沒有差太多。
不過現今的編譯器已經越來越聰明,即使我們不用 *
,它大概率也能猜出來我們是要呼叫函數。
因此下面這樣對現今大多數的電腦來說也行得通:
int main() { int (*funcPtr)(int){ &func }; funcPtr(5); return 0; }
回調函數 Callback function
函數指標一個非常普遍被運用的方式是將它當作另一個函數的參數。這個被當作參數的函數,叫做回調函數。
回調函數的功能在於提供使用者一個可以自己定義的區塊。一個很經典的例子就是 std::sort()
。
std::sort()
這個函數看名字應該就可以知道,它是目的是為了排序一串數字,預設是由小到大進行排序。
但是我們可以透過將客製化函數當作參數傳進 std::sort()
,達到更改排序的邏輯。
我們直接來看一個例子:
bool compare(int n1, int n2) { return n1 > n2; } int main() { bool (*comp)(int, int){ &compare }; std::vector vec{1, 3, 5, 2, 4}; std::sort(vec.begin(), vec.end()); std::cout << "default: "; for (auto i : vec) { std::cout << i << " "; } std::cout << std::endl; std::cout << "callback function: "; std::sort(vec.begin(), vec.end(), comp); for (auto i : vec) { std::cout << i << " "; } return 0; }
這樣的輸出會是:
default: 1 2 3 4 5 callback function: 5 4 3 2 1
我們可以看到,comp
被我們當作函數指標傳進 std::sort()
,進而更改了內部排序的邏輯。
為什麼我知道 comp
需要兩個參數並且回傳型態是 bool
呢?我們可以從 官方文件 中的「參數 Parameters」看出這點。
美化
我相信大家都覺得函數指標長得有夠醜的,一堆括弧跟星星,用起來超級不方便的。
其實我自己也這麼覺得,有時候也會不小心記錯符號。
但我們有幾個方法可以美化這個東西,讓我們用起來更方便!
- 使用 type alias
- 使用
std::function()
使用 type alias
type alias 讓我們可以將複雜的型態轉變為平易近人的名稱。
比如說:
using FuncType = int(*)(int); // 超難用 bool func(int x, int (*funcPtr)(int)); // 平易近人 bool func(int x, FuncType funcPtr);
這樣是不是就變得很平易近人啦!我們只需要打一次就可以了,剩下的使用自定義的名稱就好。
使用 std::function()
其實 C++ 也知道函數指標長得很醜,所以他們有自己建立這個存在於標准函式庫的 std::function()
給我們使用。
在使用前,我們必須引入 functional 這個函式庫。接著我們就可以這樣用:
bool func(int x, std::function<int()> funcPtr);
我自己個人還是比較喜歡使用 type alias 的做法啦,但有些人覺得使用 std::function()
對於理解上來說更直覺。
但如果說這個指標只會出現一次的話,那就用 std::function()
吧!如果會出現多次的話再考慮使用 type alias。
總結
這章我們了解了一個進階的概念,函數指標,包括它為什麼存在、它長怎麼樣、它的應用等等。
總結來說,函數指標在如果需要將函數儲存在陣列之中,或是如果需要將一個函數當作變數傳進另一個函數之中時,會變得非常實用。
那有關函數的篇章就到這裡啦!下一章我們會進入到作用域的世界~