第 18 講:指針和函數
前文我們提到了指針的很多用法,比如指針和普通變量的使用,利用指針來指向變量,以間接操作變量的方式來控制程序的行為。我們還講到了指針和數組的方式,以及把數組傳入函數的參數的時候,會退化為指針的形式,因此數組當參數傳遞到函數里的時候,是以指針形式存在的,所以我們需要單獨為其添加一個參數表示數組的長度。我們甚至還討論了字符串(字符數組)和普通數組的表現方式。
今天我們要談論一種新型語法,這種語法不多見,所以很少有人接觸到,所以比較難。但它依然很有用途。
數組有指針,那么函數呢?
很令人欣喜和疑惑的是,在 C 語言里,函數也具有指針一說。在內存里,如果我們不嘗試把代碼編譯后的二進制串放到內存里,我們就無法調用和執(zhí)行這些方法。那么唯一能找到它們執(zhí)行位置的就是通過這些函數的執(zhí)行地址來確定函數的位置了。既然是地址,就可以存在指針。所以 C 語言為函數也提供了指針的說法。
在前文里,我們已經提到了數組退化為指針作為返回值的一個說法。這種返回指針的函數叫做指針函數,它們并不是多余和難用的,因為需要堆內存這一知識點才能輔助我們更好使用它們,不過因為超過了講解范圍,所以我們沒有在上一節(jié)內容里提及到非常深入。
今天要提到的是,間接調用函數的機制。這種機制被稱為函數指針。
函數指針的用法
還記得函數需要聲明吧?聲明也被稱為定義,所以這些寫在開頭的內容被稱為“函數變量”的定義。它們被廣義化稱為一個變量,它們滿足標識符命名規(guī)則,而且也需要先定義后使用。
基本用法的實現流程
比如上述一個簡單的執(zhí)行流程,我們在外面寫的 int Add(int, int)
就是函數的定義。當我們定義了它們,我們就可以使用 Add
的函數了,否則,你必須把 Add
放在 main
前面書寫。
不過,函數指針的寫法格式有點類似于數組指針,它需要把函數名括起來,前面加上指針符號:
這就是一個函數指針。這種指針僅用于指向一個函數:
*
,取出 Adder
Adder
函數指針,來調用指向的函數 Add
,傳入兩個參數 a
和 b
來得到結果,賦值給 result
這里特別要提及的是,調用和聲明函數指針的時候,都需要這個星號。第一個星號(第 8 行)指的是定義語句的星號,表示定義的這個變量是一個函數指針變量;而第二個星號(第 11 行)則指的是調用指向函數(即取出內容)。
在 C 語言里,由于為函數指針賦值操作(int (*Adder)(int, int) = &Add
)和調用指向函數操作((*Adder)(a, b)
)兩個操作基本上都是這么書寫的,沒有其它的形式,所以指針賦值操作里的取地址符可以不需要寫,而且調用函數指針指向的函數的操作也可以不寫這個取內容符號,即上面第 8 行的代碼和第 11 行的代碼可以修改變?yōu)槿缦路绞剑?/span>
函數指針構成的數組?
下面我們?yōu)榱烁邮煜ふZ法,我們來一個難一點的。我們嘗試用一個數組來存放一組函數指針。我們之前學過,數組可以存放整數(整數數組)、小數(小數數組)和字符(字符串或字符數組)。
首先考慮如下情況,我們嘗試用四個函數來實現四則運算。
int
參數,返回 int
那么,這個數組就是
為什么函數指針數組這么書寫呢?這是因為,數組符號
[]
一般都挨在變量名稱后,所以這里也不例外。我們在原始的函數指針名后添加數組符號[]
暗示它是數組,這個時候它就可以表示一個函數指針數組了。而且,我們不用考慮函數指針左側的這個星號*
到底是作用在誰上的。因為這個運算符挨著誰,就是誰,它是單目運算符。
現在,我們?yōu)槠滟x初始值。
for
循環(huán)遍歷,然后挨個執(zhí)行和輸出。
18 -8 65 0 5
函數指針作參數
在什么場合會用到如此雞肋和別扭的寫法呢?考慮一下。前文提到的 printf
、puts
等等函數都是系統(tǒng)為我們已經提供好了的,我們無需實現,而且無法修改它們的實現。這樣的函數既然自帶,肯定考慮到了擴展性,即考慮到我們可能會在后期更改或給予一些特殊情況。函數指針就可以解決一部分問題。
如果我們嘗試為代碼寫一個冒泡排序的算法代碼,如下所示:
這樣就實現了。不過,如果我們考慮擴展性,萬一用戶要寫一個降序執(zhí)行冒泡排序的算法呢?那豈不是照抄代碼,只改掉比較的代碼(第 8 行)?
是的,實際上,我們實現僅需要修改第 8 行的比較,把 >
改為 <
就好了。不過很顯然的是,這種東西如果非得讓用戶自行實現一遍,還不如提供一個參數,讓它自己去實現比較操作。所以我們可以嘗試為這個函數添加一個函數指針作為一個參數,專門讓用戶自己實現這個比較算法,然后用函數指針來調用自己實現的這個比較就可以了:
僅需要這么修改它,用戶層面就可以自己實現和修改比較算法的代碼,來實現比較操作了。整體代碼如下:
函數指針傳參時的函數的聲明
最頭疼的是,這種函數的聲明語句應該如何寫。實際上,照著前文數組指針的格式簡寫就好,把參數名稱全去掉,符號該有的還保留就可以了:
你答對了嗎?
那么,再來一個麻煩的。我們嘗試把剛才的函數指針變量構成的數組作為函數參數傳遞進去。
答案就是
為什么可以寫作 int (*[])(int, int)
或 int (**)(int, int)
呢?這是因為,去掉函數指針的部分 int (*)(int, int)
外,這兩種寫法就剩下一個 []
和一個指針定義符 *
了,這不就恰好是一個數組的意思嗎?所以,這種寫法恰好就表示了一個函數指針變量的數組。
快速排序 qsort
函數
在 C 語言里甚至為我們貼心地提供了為數值序列排序的函數 qsort
,不過這個函數用起來非常復雜,需要函數指針。
先來看看使用
這樣調用它就可以立馬得到排序后的數組,和上面自己實現的冒泡排序的操作基本上一樣,不過差別是需要添加的函數指針,有一些奇怪。
這個函數第一個參數傳入的是數組名(即數組的首地址),而第二個參數傳入的是數組的長度 9。
第三個參數和第四個參數比較麻煩。第三個參數傳入的是數組的每一個元素的內存大小,顯然這個數組是 int
類型的,所以需要指定這個數組的元素大小是 sizeof(int)
,而第四個參數傳入的是比較函數。這里是在上面實現的 cmp
函數,所以這里寫 &cmp
或直接寫 cmp
。
下面來分析一下自己實現的函數 cmp
到底為什么參數這么奇怪。
void *
類型
const
是一個關鍵字,表示指向不可修改,所以這里我們先可以忽略掉。那么,void *
是什么類型呢?void *
往往被稱為無類型指針,這種類型允許你在傳入的時候傳入任何指針類型(int *
、char *
等都可以)。不過在使用取值的時候,由于指針無法確定指向的元素類型,所以需要你自己給出強制轉換,它就會把這個類型轉換為指定的類型的指針。例如
第 3 行將會輸出這個 p
指針的地址數值,而第 4 行將在取出地址的前提下,使用 *
間接取出數值,得到 3 這個結果。注意,*(int *)p
和 *((int *)p)
是等價的,你無需為后面這一坨內容添加括號。
那么,我們轉回 qsort
函數就可以看到,整個函數的聲明是這樣的:
從注釋里就可以看到,第四個參數里為什么傳入的兩個參數是 const void *
類型了:因為兩個參數都指向元素,而這兩個元素的類型我們是無法從第一個參數 void *
類型而確定下來的,所以我們不得不推后到調用時候才能確定,所以這個時候,我們不得不為其設置 void *
類型來表示這個指向的元素是無類型(未知類型)的。
那么,從實現角度上說,cmp
函數里給定的代碼是:
由此可以看出,我們如果需要強制得到元素的類型,需要把指針 a
和 b
轉換為 int *
類型才可以去使用和取值。然后,我們轉換后,把地址數值再使用 *
運算符取出元素數值,將兩者相減就可以得到它們倆的大小關系了。當然,如果需要降序排序,只需要把被減數和減數換一下位置,即改為 *(int *)b - *(int *)a
即可。
總的來說,void *
類型是一個可以指向任何類型變量的指針,但需要確定指向元素的數值時,需要自己給定指針的類型,并通過間接取值的運算符 *
來取值:
即強制轉換 void *
為其它類型的指針 T *
。
那么 NULL
常量?
前文提到了一個 NULL
常量,這個常量表示為所有指針類型的默認數值,即指向 0 號內存的指針。當我們現在已經學會了 void *
類型后,就可以知道 NULL
的本面目:(void *)0
,它直接把 0 強制轉換為一個地址,而這個地址還是無類型的。所以這允許了所有的類型都可以用。所以,NULL
和 (void *)0
等價。
總結
那么至此,我們就把所有關于指針的內容就介紹完了?,F在我們作出總結。

這些寫法都在函數聲明時去掉變量名 p
即可。