代码也住在内存里(第 2 章的 Text 段),所以代码也有地址;存函数地址的指针,就是函数指针。它是 C 语言实现「把行为当数据传」的唯一手段:qsort 的比较器、信号处理函数、跳转表、乃至下一章结构体的「面向对象雏形」,全是它。本章把它从「声明最吓人的语法」变成「你手里最顺的工程工具」。原文小节编号错成 10.x,一并修正。
int add(int a, int b) { return a + b; }
int (*f)(int, int) = add; // f:指向「(int,int) 返回 int 的函数」的指针
int r = f(3, 4); // 通过指针调用,7
读法沿第 5 章的老规矩(从内向外):括号里 (*f) 先声明 f 是指针,外面 (int, int) 是参数、int 是返回值。括号省不得:int *f(int, int)(没有括号包 f)意思是「f 是一个函数,返回 int*」,两者差一个语义层。函数名自带退化(像数组名,第 4 章):f = add 和 f = &add 等价,f(3,4) 和 (*f)(3,4) 也等价,写简洁版就好。
裸声明太丑,typedef 一步到位:
typedef int (*BinOp)(int, int); // BinOp 现在是个类型名
BinOp f = add; // 从此告别天书
函数指针最伟大的应用是回调:通用算法把「怎么比较」「怎么处理」留给调用者注入。标准库样板 qsort:
int cmp_int(const void *a, const void *b) { // 统一签名,void* 进
int x = *(const int *)a, y = *(const int *)b;
return (x > y) - (x < y); // 负/零/正;别写 x-y,int 最小值溢出是真实事故
}
int arr[] = {3, 1, 4, 1, 5};
qsort(arr, 5, sizeof(int), cmp_int);
注意这套设计的分工:void* 解决「不知道元素类型」(第 1 章的通用插座),比较函数指针解决「不知道大小关系」,qsort 自己只管算法骨架。三块拼起来,C 语言版的泛型就位。这个范式在信号处理(signal(SIGINT, handler))、线程(pthread_create 的入口函数)、事件循环里反复出现:框架定骨架,你注入血肉。
把函数指针放进数组,用下标派发:
typedef int (*Op)(int, int);
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
Op ops[] = { add, sub, mul };
int r = ops[1](10, 3); // sub(10,3) = 7
对比一串 if (op=="+") ... else if (op=="-") ...,跳转表三个优点:O(1) 派发(下标直达)、新增操作只加一行表项(开闭原则的 C 版)、语义集中(看表就知道系统有什么行为)。典型用途:菜单命令分发、协议解析的包处理表、状态机的转移表(状态 × 事件 的二维函数指针数组)。工程细节:表项可以加名字注释或用 designated initializer ops = { ['+'] = add, ['-'] = sub },可读性再上一档。
函数可以返回函数指针,裸声明是天书,typedef 之后稀松平常:
BinOp select(char op) {
switch (op) {
case '+': return add;
case '-': return sub;
}
return NULL; // 别忘了失败路径
}
int r = select('*') == NULL ? 0 : 0; // 先判空再调用!
工程提醒:返回的函数指针和所有指针一样有 NULL 状态,调用前判空;跳转表里也是同理,「表里混进一个 NULL」的崩溃会发生在最深的调用栈里,初始化表时宁可先全填一个默认函数。
函数指针在机器层就是「间接调用」:call [rax](地址在寄存器里),而直接调用是 call 0x401234(地址写死在指令里)。间接调用的代价:分支预测器(计组课第 6 章)对固定目标的预测容易,对变化的间接目标难,虚函数、回调密集的热循环会慢一点;编译器也因此难以内联。这是 C++ 虚函数、Java 接口调用都有的「动态派发税」。所以性能敏感的内核路径里,常见手法是「热点判断 + 内联直调,冷路径才走函数指针」,理解了机制,这类优化一看就懂。
int *f(int,int) 是函数不是指针;判别法:声明里 * 是否被括号和标识符绑在一起。const void * 签名是唯一正解。(x>y)-(x<y)。void qsort_by(void *base, size_t n, size_t size, int (*cmp)(const void*, const void*)) 的插入排序版,分别用 int、double、字符串三种比较器排序,验证泛型性。select(char op) 返回函数指针,main 里读「3 + 4」式输入并求值;把四则运算表改成 designated initializer 写法。