函式指標是 C 語言最強大也最容易令初學者卻步的特性之一。它讓「行為」本身變成可以傳遞、儲存、替換的資料,
是實作回呼(callback)、命令分派(dispatch table)、外掛架構(plugin)與泛型演算法(如 qsort)的基礎。
本單元以 K&R §5.11 的通用排序程式為主軸,完整介紹函式指標的語法與實務應用。
學習目標
- 理解「函式名稱在運算式中會退化為函式指標」的規則
- 能正確宣告、初始化、呼叫函式指標
- 能將函式指標作為參數傳遞,實作回呼(callback)機制
- 熟悉標準函式庫
qsort的介面,能撰寫符合規格的比較函式 - 能使用函式指標陣列建立命令分派表(jump table / dispatch table)
- 會用
typedef簡化函式指標型別,提高可讀性
對照章節 / 參考來源
- K&R《The C Programming Language, 2nd Edition》§5.11 Pointers to Functions(通用排序程式)
- K&R §5.12 Complicated Declarations(複雜宣告,詳見下一單元)
- C11 標準 §6.3.2.1(函式指定子的轉換)、§7.22.5.2(
qsort) man 3 qsort
觀念講解
1. 函式指標的宣告與基本語法
函式也佔據記憶體(程式碼段),因此可以有位址。指向「接收兩個 int、回傳 int 的函式」的指標宣告如下:
int (*fp)(int, int); /* fp 是函式指標 */
注意括號不可省略:
int *fp(int, int); /* 這是「回傳 int* 的函式」的宣告,不是指標! */
int (*fp)(int, int); /* 這才是「指向函式的指標」 */
指派與呼叫:
int add(int a, int b) { return a + b; }
int (*fp)(int, int) = add; /* 函式名稱自動轉成位址,&add 亦可 */
int r1 = fp(3, 4); /* 直接呼叫(現代慣用寫法) */
int r2 = (*fp)(3, 4); /* 先解參考再呼叫(K&R 傳統寫法),效果相同 */
add、&add、*add、**add 在運算式中通通等價——因為函式名稱退化為指標後,
對函式指標解參考又得到函式指定子,再退化回指標,如此循環。這是 C 的特殊規則。
2. 函式指標作為參數:回呼(callback)
把函式指標傳入另一個函式,讓「呼叫者決定部分行為」,這種模式稱為回呼:
/* 對陣列每個元素套用 f,將行為參數化 */
void map_int(int *a, int n, int (*f)(int))
{
for (int i = 0; i < n; i++)
a[i] = f(a[i]);
}
呼叫端可以傳入 square、negate、double_it 等不同函式,map_int 本身完全不需修改。
這就是「策略模式」在 C 語言的原生實現,也是事件驅動程式(GUI、網路伺服器)的核心機制。
3. qsort 與比較函式(K&R §5.11 的現代版)
K&R §5.11 自己寫了一個通用 qsort;現代 C 直接用 <stdlib.h> 提供的版本:
void qsort(void *base, size_t nmemb, size_t size,
int (*compar)(const void *, const void *));
比較函式收到的是「指向元素的 const void*」,必須自己轉型回真正的型別:
int cmp_int(const void *a, const void *b)
{
int x = *(const int *)a, y = *(const int *)b;
return (x > y) - (x < y); /* 避免 x - y 溢位的慣用寫法 */
}
注意:排序字串陣列(char *arr[])時,比較函式收到的是 char **,
需要寫成 *(char *const *)a,這是最常見的 qsort 誤用點。
4. 函式指標陣列:命令分派表
把多個「同型別」的函式指標放進陣列,用索引或查表選擇要執行的函式,
可以取代冗長的 switch,且新增功能只需在表裡加一列:
typedef int (*op_fn)(int, int);
struct command {
const char *name;
op_fn fn;
};
struct command table[] = {
{ "add", op_add },
{ "sub", op_sub },
{ "mul", op_mul },
};
實務應用:直譯器的位元組碼分派、shell 內建命令、選單系統、狀態機的狀態轉移表。
5. typedef 簡化函式指標型別
沒有 typedef 時,「回傳函式指標的函式」宣告非常難讀:
int (*get_op(char c))(int, int); /* 天書 */
用 typedef 之後:
typedef int (*op_fn)(int, int);
op_fn get_op(char c); /* 一目了然 */
建議:專案中只要函式指標出現超過一次,就應該 typedef。命名慣例常用 _fn、_cb、_handler 結尾。
6. 函式指標與資料指標的差異
- 標準不保證函式指標可以與
void *互轉(POSIX 的dlsym是例外,實務上多數平台可行)。 - 函式指標之間可以互相轉型,但用「不相容的型別」呼叫是未定義行為(UB)。
- 比較函式指標是否相等(
==、!=)是合法的;比大小則不合法。
範例程式
| 檔案 | 內容 | 編譯 / 執行 |
|---|---|---|
01_basics.c |
宣告、初始化、兩種呼叫語法、等價性示範、typedef | cc -std=c11 -Wall -Wextra -o out 01_basics.c && ./out |
02_callbacks.c |
map/filter 回呼、qsort 排序整數/字串/結構、多鍵排序 |
cc -std=c11 -Wall -Wextra -o out 02_callbacks.c && ./out |
03_dispatch_table.c |
函式指標陣列、命令分派表實作簡易計算指令直譯器 | cc -std=c11 -Wall -Wextra -o out 03_dispatch_table.c && ./out |
所有程式都內建示範資料,直接執行即可看到輸出,不需要任何互動。
常見錯誤與陷阱
- 括號放錯位置:
int *f(int)是函式宣告,int (*f)(int)才是指標。編譯器對前者不會報錯, 直到你想指派位址給它才發現不對勁。 qsort比較函式轉型錯誤:排序char *arr[]時把參數當char *用(實際是char **), 結果是比較指標值的位元組而非字串內容,排序結果看似隨機。- 比較函式用
x - y:兩個很大的int相減會 signed overflow(UB)。 正確寫法是(x > y) - (x < y)。 - 呼叫尚未初始化的函式指標:野指標呼叫直接跳到垃圾位址,通常是立即 crash(bus error / segfault),
宣告時務必初始化為具名函式或
NULL並在呼叫前檢查。 - 簽名不相容仍強制轉型呼叫:例如把
int f(void)轉成int (*)(int)再呼叫是 UB, 在某些 ABI 上恰好可動,換平台就爆炸。
練習題
- 【易】寫一個
int apply(int x, int (*f)(int)),並用它套用abs、平方、加一等三種函式。 - 【易】用
qsort將double陣列由大到小排序(注意浮點數比較不能用相減)。 - 【中】寫
void for_each(void *base, size_t n, size_t size, void (*visit)(void *)), 模仿qsort的泛型介面,對任意型別陣列的每個元素呼叫visit。 - 【中】為結構
struct student { char name[16]; int score; }實作兩個比較函式: 先依分數遞減、同分依姓名字典序遞增,用單一qsort完成多鍵排序。 - 【難】實作一個迷你「狀態機」:狀態以
enum表示,轉移表為state_fn table[N_STATES],每個狀態函式回傳下一個狀態,主迴圈只有三行。 - 【難】仿 K&R §5.11 自己實作泛型
my_qsort(用插入排序即可), 介面與標準qsort完全相同,需以char *位移與memcpy做元素交換。
補充:效能與設計備忘
- 函式指標呼叫是「間接呼叫」,CPU 無法在編譯期確定目標, 分支預測器要多做工;熱路徑上的間接呼叫可能比直接呼叫慢, 但差距通常遠小於它換來的架構彈性——先量測再最佳化。
- 現代編譯器在看得到目標的情況下能把函式指標「去虛擬化」 (devirtualization)成直接呼叫甚至內聯。
- 回呼介面通常會多帶一個
void *user_data參數(context pointer), 讓呼叫端夾帶自己的狀態——標準的qsort_r(BSD/GNU 擴充)就是為此而生。 - 命名慣例小結:型別以
_fn/_cb結尾、表格用const靜態陣列、 查無指令回傳NULL並由呼叫端統一處理。
延伸閱讀
- K&R §5.11–5.12:通用排序與複雜宣告
- 《Expert C Programming》(Peter van der Linden) 第 3 章:解讀 C 宣告
- cppreference:Pointer declaration — Pointers to functions
man 3 qsort、man 3 bsearch(同樣使用比較函式的二分搜尋)- glibc 原始碼中
qsort的實作(實際上是 merge sort / introsort 混合)