本文寫的非常詳細,因為我想為初學者建立一個意識模型,來幫助他們理解函式指標的語法和基礎。如果你不討厭事無鉅細,請盡情閱讀吧。
函式指標雖然在語法上讓人有些迷惑,但不失為一種有趣而強大的工具。本文將從C語言函式指標的基礎開始介紹,再結合一些簡單的用法和關於函式名稱和地址的趣聞。在最後,本文給出一種簡單的方式來看待函式指標,讓你對其用法有一個更清晰的理解。
函式指標和一個簡單的函式
我們從一個非常簡單的”Hello World“函式入手,來見識一下怎樣建立一個函式指標。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <stdio.h> // 函式原型 void sayHello(); //函式實現 void sayHello(){ printf("hello world\n"); } // main函式呼叫 int main() { sayHello(); } |
我們定義了一個名為sayHello的函式,它沒有返回值也不接受任何引數。當我們在main函式中呼叫它的時候,它向螢幕輸出出”hello world“。非常簡單。接下來,我們改寫一下main函式,之前直接呼叫的sayHello函式,現在改用函式指標來呼叫它。
1 2 3 4 |
int main() { void (*sayHelloPtr)() = sayHello; (*sayHelloPtr)(); } |
第二行void (*sayHelloPtr)()
的語法看起來有些奇怪,我們來一步一步分析。
- 這裡,關鍵字
void
的作用是說我們建立了一個函式指標,並讓它指向了一個返回void(也就是沒有返回值)的函式。 - 就像其他任何指標都必須有一個名稱一樣,這裡
sayHelloPtr
被當作這個函式指標的名稱。 - 我們用
*
符號來表示這是一個指標,這跟宣告一個指向整數或者字元的指標沒有任何區別。 *sayHelloPtr
兩端的括號是必須的,否則,上述宣告變成void *sayHelloPtr()
,*
會優先跟void
結合,變成了一個返回指向void的指標的普通函式的宣告。因此,函式指標宣告的時候不要忘記加上括號,這非常關鍵。- 引數列表緊跟在指標名之後,這個例子中由於沒有引數,所以是一對空括號
()
。 - 將上述要點結合起來,
void (*syaHelloPtr)()
的意義就非常清楚了,這是一個函式指標,它指向一個不接收引數且沒有返回值的函式。
在上面的第二行程式碼,即void (*sayHelloPtr)() = sayHello;
,我們將sayHello這個函式名賦給了我們新建的函式指標。關於函式名的更多細節我們會在下文中討論,現在暫時可以將其看作一個標籤,它代表函式的地址,並且可以賦值給函式指標。這就跟語句int *x = &myint;
中我們把myint的地址賦給一個指向整數的指標一樣。只是當我們考慮函式的時候,我們不需要加上一個取地址符&
。簡而言之,函式名就是它的地址。接著看第三行,我們用程式碼’(*sayHelloPtr)();·‘解引用並呼叫了函式指標。
- 在第二行被宣告之後,sayHelloPtr作為函式指標的名稱,跟其他任何指標沒有差別,能夠儲值和賦值。
- 我們對sayHelloPtr解引用的方式也與其他任何指標一樣,即在指標之前使用解引用符
*
,也就是程式碼中的*sayHelloPtr
。 - 同樣的,我們需要在其兩端加上括號,即
(*sayHelloPtr)
,否則它就不被當做一個函式指標。因此,記得宣告和解引用的時候都要在兩端加上括號。 - 括號操作符用於C語言中的函式呼叫,如果有引數參與,就將其放入括號中。這對於函式指標也是相似的,即程式碼中的
(*sayHelloPtr)()
。 - 這個函式沒有返回值,也就沒有必要將它賦值給任何變數。單獨來說,這個呼叫跟
sayHello()
沒什麼兩樣。
接下來,我們再對函式稍加修改。你會看到函式指標奇怪的語法,以及用呼叫普通函式的方法來呼叫賦值後函式指標的現象。
1 2 3 4 |
int main() { void (*sayHelloPtr)() = sayHello; sayHelloPtr(); } |
跟之前一樣,我們將sayHello函式賦給函式指標。但是這一次,我們用呼叫普通函式的方法呼叫了它。稍後討論函式名的時候我會解釋這一現象,現在只需要知道(*syaHelloPtr)()
和syaHelloPtr()
是相同的即可。
帶引數的函式指標
好了,這一次我們來建立一個新的函式指標吧。它指向的函式仍然不返回任何值,但有了引數。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include <stdio.h> //函式原型 void subtractAndPrint(int x, int y); //函式實現 void subtractAndPrint(int x, int y) { int z = x - y; printf("Simon says, the answer is: %d\n", z); } //main函式呼叫 int main() { void (*sapPtr)(int, int) = subtractAndPrint; (*sapPtr)(10, 2); sapPtr(10, 2); } |
跟之前一樣,程式碼包括函式原型,函式實現和在main函式中通過函式指標執行的語句。原型和實現中的特徵標變了,之前的sayHello函式不接受任何引數,而這次的函式subtractAndPrint接受兩個int作為引數。它將兩個引數做一次減法,然後輸出到螢幕上。
- 在第14行,我們通過'(*sapPtr)(int, int)’建立了sapPtr這個函式指標,與之前的區別僅僅是用
(int, int)
代替了原來的空括號。而這與新函式的特徵標相符。 - 在第15行,解引用和執行函式的方式與之前完全相同,只是在括號中加入了兩個引數,變成了
(10, 2)
。 - 在第16行,我們用呼叫普通函式的方法呼叫了函式指標。
帶引數且有返回值的函式指標
這一次,我們把subtractAndPrint函式改成一個名為subtract的函式,讓它把原本輸出到螢幕上的結果作為返回值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include <stdio.h> // 函式原型 int subtract(int x, int y); // 函式實現 int subtract(int x, int y) { return x - y; } // main函式呼叫 int main() { int (*subtractPtr)(int, int) = subtract; int y = (*subtractPtr)(10, 2); printf("Subtract gives: %d\n", y); int z = subtractPtr(10, 2); printf("Subtract gives: %d\n", z); } |
這與subtractAndPrint函式非常相似,只是subtract函式返回了一個整數而已,特徵標也理所當然的不一樣了。
- 在第13行,我們通過
int (*subtractPtr)(int, int)
建立了subtractPtr這個函式指標。與上一個例子的區別只是把void換成了int來表示返回值。而這與subtract函式的特徵標相符。 - 在在第15行,解引用和執行這個函式指標,除了將返回值賦值給了y以外,與呼叫subtractAndPrint沒有任何區別。
- 在第16行,我們向螢幕輸出了返回值。
- 18到19行,我們用呼叫普通函式的方法呼叫了函式指標,並且輸出了結果。
這跟之前沒什麼兩樣,我們只是加上了返回值而已。接下來我們看看另一個稍微複雜點兒的例子——把函式指標作為引數傳遞給另一個函式。
把函式指標作為引數來傳遞
我們已經瞭解過了函式指標宣告和執行的各種情況,不論它是否帶引數,或者是否有返回值。接下來我們利用一個函式指標來根據不同的輸入執行不同的函式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
#include <stdio.h> // 函式原型 int add(int x, int y); int subtract(int x, int y); int domath(int (*mathop)(int, int), int x, int y); // 加法 x+ y int add(int x, init y) { return x + y; } // 減法 x - y int subtract(int x, int y) { return x - y; } // 根據輸入執行函式指標 int domath(int (*mathop)(int, int), int x, int y) { return (*mathop)(x, y); } // main函式呼叫 int main() { // 用加法呼叫domath int a = domath(add, 10, 2); printf("Add gives: %d\n", a); // 用減法呼叫domath int b = domath(subtract, 10, 2); printf("Subtract gives: %d\n", b); } |
我們來一步一步分析。
- 我們有兩個特徵標相同的函式,add和subtract,它們都返回一個整數並接受兩個整數作為引數。
- 在第六行,我們定義了函式
int domath(int (*mathop)(int, int), int x, int y)
。它第一個引數int (*mathop)(int, int)
是一個函式指標,指向返回一個整數並接受兩個整數作為引數的函式。這就是我們之前見過的語法,沒有任何不同。它的後兩個整數引數則作為簡單的輸入。因此,這是一個接受一個函式指標和兩個整數作為引數的函式。 - 19到21行,domath函式將自己的後兩個整數引數傳遞給函式指標並呼叫它。當然,也可以像這麼呼叫。
mathop(x, y);
- 27到31行出現了我們沒見過的程式碼。我們用函式名作為引數呼叫了domath函式。就像我之前說過的,函式名是函式的地址,而且能代替函式指標使用。
main函式呼叫了兩次domath函式,一次用了add,一次用了subtract,並輸出了這兩次結果。
函式名和地址
既然有約在先,那我們就討論一下函式名和地址作為結尾吧。一個函式名(或稱標籤),被轉換成了一個指標本身。這表明在函式指標被要求當作輸入的地方,就能夠使用函式名。這也導致了一些看起來很糟糕的程式碼卻能夠正確的執行。瞧瞧下面這個例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
#include <stdio.h> // 函式原型 void add(char *name, int x, int y); // 加法 x + y void add(char *name, int x, int y) { printf("%s gives: %d\n", name, x + y); } // main函式呼叫 int main() { // 一些糟糕的函式指標賦值 void (*add1Ptr)(char*, int, int) = add; void (*add2Ptr)(char*, int, int) = *add; void (*add3Ptr)(char*, int, int) = &add; void (*add4Ptr)(char*, int, int) = **add; void (*add5Ptr)(char*, int, int) = ***add; // 仍然能夠正常執行 (*add1Ptr)("add1Ptr", 10, 2); (*add2Ptr)("add2Ptr", 10, 2); (*add3Ptr)("add3Ptr", 10, 2); (*add4Ptr)("add4Ptr", 10, 2); (*add5Ptr)("add5Ptr", 10, 2); // 當然,這也能執行 add1Ptr("add1PtrFunc", 10, 2); add2Ptr("add2PtrFunc", 10, 2); add3Ptr("add3PtrFunc", 10, 2); add4Ptr("add4PtrFunc", 10, 2); add5Ptr("add5PtrFunc", 10, 2); } |
這是一個簡單的例子。執行這段程式碼,你會看到每個函式指標都會執行,只是會收到一些關於字元轉換的警告。但是,這些函式指標都能正常工作。
- 在第15行,add作為函式名,返回這個函式的地址,它被隱式的轉換為一個函式指標。我之前提到過,在函式指標被要求當作輸入的地方,就能夠使用函式名。
- 在第16行,解引用符作用於add之前,即
*add
,在返回在這個地址的函式。之後跟函式名一樣,它被隱式的轉換為一個函式指標。 - 在第17行,取地址符作用於add之前,即
&add
,返回這個函式的地址,之後又得到一個函式指標。 - 18到19行,add不斷地解引用自身,不斷返回函式名,並被轉換為函式指標。到最後,它們的結果都和函式名沒有區別。
顯然,這段程式碼不是優秀的例項程式碼。我們從中收穫到了如下知識:其一,函式名會被隱式的轉換為函式指標,就像作為引數傳遞的時候,陣列名被隱式的轉換為指標一樣。在函式指標被要求當作輸入的任何地方,都能夠使用函式名。其二,解引用符*
和取地址符&
用在函式名之前基本上都是多餘的。
總結
我希望本文幫助你們認清了函式指標以及它的用途。只要你掌握了函式指標,它就是C語言中一個強大的工具。我也許會在以後的文章中講述更多函式指標的細節用法,包括回撥和C語言中基本的物件導向等等。
更新1
我刪掉了關於描述(*sayHelloPrt)(void)
跟(*sayHelloPrt)()
相同的那一部分,那其實是錯誤的。在評論區中,Dave G給出了一個關於這個問題很好的解釋。