# 練習18:函數指針
> 原文:[Exercise 18: Pointers To Functions](http://c.learncodethehardway.org/book/ex18.html)
> 譯者:[飛龍](https://github.com/wizardforcel)
函數在C中實際上只是指向程序中某一個代碼存在位置的指針。就像你創建過的結構體指針、字符串和數組那樣,你也可以創建指向函數的指針。函數指針的主要用途是向其他函數傳遞“回調”,或者模擬類和對象。在這歌1練習中我們會創建一些回調,并且下一節我們會制作一個簡單的對象系統。
函數指針的格式類似這樣:
```c
int (*POINTER_NAME)(int a, int b)
```
記住如何編寫它的一個方法是:
+ 編寫一個普通的函數聲明:`int callme(int a, int b)`
+ 將函數用指針語法包裝:`int (*callme)(int a, int b)`
+ 將名稱改成指針名稱:`int (*compare_cb)(int a, int b)`
這個方法的關鍵是,當你完成這些之后,指針的變量名稱為`compare_cb`,而你可以將它用作函數。這類似于指向數組的指針可以表示所指向的數組。指向函數的指針也可以用作表示所指向的函數,只不過是不同的名字。
```c
int (*tester)(int a, int b) = sorted_order;
printf("TEST: %d is same as %d\n", tester(2, 3), sorted_order(2, 3));
```
即使是對于返回指針的函數指針,上述方法依然有效:
+ 編寫:`char *make_coolness(int awesome_levels)`
+ 包裝:`char *(*make_coolness)(int awesome_levels)`
+ 重命名:`char *(*coolness_cb)(int awesome_levels)`
需要解決的下一個問題是使用函數指針向其它函數提供參數比較困難,比如當你打算向其它函數傳遞回調函數的時候。解決方法是使用`typedef`,它是C的一個關鍵字,可以給其它更復雜的類型起個新的名字。你需要記住的事情是,將`typedef`添加到相同的指針語法之前,然后你就可以將那個名字用作類型了。我使用下面的代碼來演示這一特性:
```c
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
/** Our old friend die from ex17. */
void die(const char *message)
{
if(errno) {
perror(message);
} else {
printf("ERROR: %s\n", message);
}
exit(1);
}
// a typedef creates a fake type, in this
// case for a function pointer
typedef int (*compare_cb)(int a, int b);
/**
* A classic bubble sort function that uses the
* compare_cb to do the sorting.
*/
int *bubble_sort(int *numbers, int count, compare_cb cmp)
{
int temp = 0;
int i = 0;
int j = 0;
int *target = malloc(count * sizeof(int));
if(!target) die("Memory error.");
memcpy(target, numbers, count * sizeof(int));
for(i = 0; i < count; i++) {
for(j = 0; j < count - 1; j++) {
if(cmp(target[j], target[j+1]) > 0) {
temp = target[j+1];
target[j+1] = target[j];
target[j] = temp;
}
}
}
return target;
}
int sorted_order(int a, int b)
{
return a - b;
}
int reverse_order(int a, int b)
{
return b - a;
}
int strange_order(int a, int b)
{
if(a == 0 || b == 0) {
return 0;
} else {
return a % b;
}
}
/**
* Used to test that we are sorting things correctly
* by doing the sort and printing it out.
*/
void test_sorting(int *numbers, int count, compare_cb cmp)
{
int i = 0;
int *sorted = bubble_sort(numbers, count, cmp);
if(!sorted) die("Failed to sort as requested.");
for(i = 0; i < count; i++) {
printf("%d ", sorted[i]);
}
printf("\n");
free(sorted);
}
int main(int argc, char *argv[])
{
if(argc < 2) die("USAGE: ex18 4 3 1 5 6");
int count = argc - 1;
int i = 0;
char **inputs = argv + 1;
int *numbers = malloc(count * sizeof(int));
if(!numbers) die("Memory error.");
for(i = 0; i < count; i++) {
numbers[i] = atoi(inputs[i]);
}
test_sorting(numbers, count, sorted_order);
test_sorting(numbers, count, reverse_order);
test_sorting(numbers, count, strange_order);
free(numbers);
return 0;
}
```
在這段程序中,你將創建動態排序的算法,它會使用比較回調對整數數組排序。下面是這個程序的分解,你應該能夠清晰地理解它。
ex18.c:1~6
通常的包含,用于所調用的所有函數。
ex18.c:7~17
這就是之前練習的`die`函數,我將它用于錯誤檢查。
ex18.c:21
這是使用`typedef`的地方,在后面我像`int`或`char`類型那樣,在`bubble_sort`和`test_sorting`中使用了`compare_cb`。
ex18.c:27~49
一個冒泡排序的實現,它是整數排序的一種不高效的方法。這個函數包含了:
ex18.c:27
這里是將`typedef`用于` compare_cb`作為`cmp`最后一個參數的地方。現在它是一個會返回兩個整數比較結果用于排序的函數。
ex18.c:29~34
棧上變量的通常創建語句,前面是使用`malloc`創建的堆上整數數組。確保你理解了`count * sizeof(int)`做了什么。
ex18.c:38
冒泡排序的外循環。
ex18.c:39
冒泡排序的內循環。
ex18.c:40
現在我調用了`cmp`回調,就像一個普通函數那樣,但是不通過預先定義好的函數名,而是一個指向它的指針。調用者可以像它傳遞任何參數,只要這些參數符合`compare_cb` `typedef`的簽名。
ex18.c:41-43
冒泡排序所需的實際交換操作。
ex18.c:48
最后返回新創建和排序過的結果數據`target`。
ex18.c:51-68
`compare_cb`函數類型三個不同版本,它們需要和我們所創建的`typedef`具有相同的定義。否則C編輯器會報錯說類型不匹配。
ex18.c:74-87
這是`bubble_sort`函數的測試。你可以看到我同時將`compare_cb`傳給了`bubble_sort`來演示它是如何像其它指針一樣傳遞的。
ex18.c:90-103
一個簡單的主函數,基于你通過命令行傳遞進來的整數,創建了一個數組。然后調用了`test_sorting`函數。
ex18.c:105-107
最后,你會看到`compare_cb`函數指針的`typedef`是如何使用的。我僅僅傳遞了`sorted_order`、`reverse_order`和`strange_order`的名字作為函數來調用`test_sorting`。C編譯器會找到這些函數的地址,并且生成指針用于`test_sorting`。如果你看一眼`test_sorting`你會發現它把這些函數傳給了`bubble_sort`,并不關心它們是做了什么。只要符合`compare_cb`原型的東西都有效。
ex18.c:109
我們在最后釋放了我們創建的整數數組。
## 你會看到什么
運行這個程序非常簡單,但是你要嘗試不同的數字組合,甚至要嘗試輸入非數字來看看它做了什么:
```sh
$ make ex18
cc -Wall -g ex18.c -o ex18
$ ./ex18 4 1 7 3 2 0 8
0 1 2 3 4 7 8
8 7 4 3 2 1 0
3 4 2 7 1 0 8
$
```
## 如何使它崩潰
我打算讓你做一些奇怪的事情來使它崩潰,這些函數指針都是類似于其它指針的指針,他們都指向內存的一塊區域。C中可以將一種指針的指針轉換為另一種,以便以不同方式處理數據。這些通常是不必要的,但是為了想你展示如何侵入你的電腦,我希望你把這段代碼添加在`test_sorting`下面:
```c
unsigned char *data = (unsigned char *)cmp;
for(i = 0; i < 25; i++) {
printf("%02x:", data[i]);
}
printf("\n");
```
這個循環將你的函數轉換成字符串,并且打印出來它的內容。這并不會中斷你的程序,除非CPU和OS在執行過程中遇到了問題。在它打印排序過的數組之后,你所看到的是一個十六進制數字的字符串:
```
55:48:89:e5:89:7d:fc:89:75:f8:8b:55:fc:8b:45:f8:29:d0:c9:c3:55:48:89:e5:89:
```
這就應該是函數的原始的匯編字節碼了,你應該能看到它們有相同的起始和不同的結尾。也有可能這個循環并沒有獲得函數的全部,或者獲得了過多的代碼而跑到程序的另外一片空間。這些不通過更多分析是不可能知道的。
## 附加題
+ 用十六進制編輯器打開`ex18`,接著找到函數起始處的十六進制代碼序列,看看是否能在原始程序中找到函數。
+ 在你的十六進制編輯器中找到更多隨機出現的東西并修改它們。重新運行你的程序看看發生了什么。字符串是你最容易修改的東西。
+ 將錯誤的函數傳給`compare_cb`,并看看C編輯器會報告什么錯誤。
+ 將`NULL`傳給它,看看程序中會發生什么。然后運行`Valgrind`來看看它會報告什么。
+ 編寫另一個排序算法,修改`test_sorting`使它接收任意的排序函數和排序函數的比較回調。并使用它來測試兩種排序算法。
- 笨辦法學C 中文版
- 前言
- 導言:C的笛卡爾之夢
- 練習0:準備
- 練習1:啟用編譯器
- 練習2:用Make來代替Python
- 練習3:格式化輸出
- 練習4:Valgrind 介紹
- 練習5:一個C程序的結構
- 練習6:變量類型
- 練習7:更多變量和一些算術
- 練習8:大小和數組
- 練習9:數組和字符串
- 練習10:字符串數組和循環
- 練習11:While循環和布爾表達式
- 練習12:If,Else If,Else
- 練習13:Switch語句
- 練習14:編寫并使用函數
- 練習15:指針,可怕的指針
- 練習16:結構體和指向它們的指針
- 練習17:堆和棧的內存分配
- 練習18:函數指針
- 練習19:一個簡單的對象系統
- 練習20:Zed的強大的調試宏
- 練習21:高級數據類型和控制結構
- 練習22:棧、作用域和全局
- 練習23:認識達夫設備
- 練習24:輸入輸出和文件
- 練習25:變參函數
- 練習26:編寫第一個真正的程序
- 練習27:創造性和防御性編程
- 練習28:Makefile 進階
- 練習29:庫和鏈接
- 練習30:自動化測試
- 練習31:代碼調試
- 練習32:雙向鏈表
- 練習33:鏈表算法
- 練習34:動態數組
- 練習35:排序和搜索
- 練習36:更安全的字符串
- 練習37:哈希表
- 練習38:哈希算法
- 練習39:字符串算法
- 練習40:二叉搜索樹
- 練習41:將 Cachegrind 和 Callgrind 用于性能調優
- 練習42:棧和隊列
- 練習43:一個簡單的統計引擎
- 練習44:環形緩沖區
- 練習45:一個簡單的TCP/IP客戶端
- 練習46:三叉搜索樹
- 練習47:一個快速的URL路由
- 后記:“解構 K&R C” 已死