本文寫的非常詳細,因爲我想爲初學者建立一個意識模型,來幫助他們理解函數指針的語法和基礎。如果你不討厭事無鉅細,請盡情閱讀吧。
函數指針雖然在語法上讓人有些迷惑,但不失爲一種有趣而強大的工具。本文將從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給出了一個關於這個問題很好的解釋。