核心概念:从基础到进阶
要理解“函数指针的指针”,我们得一步步来:

第 0 步:普通变量和指针
- 普通变量:存储一个值,
int a = 10;。a存储了整数10。 - 指针:存储一个内存地址。
int *p = &a;。p存储了变量a的内存地址。
第 1 步:函数指针
- 函数:一段代码,有名字和返回类型、参数列表。
int add(int x, int y)。 - 函数指针:一个指向函数的指针,它存储的不是普通数据,而是函数的入口地址,通过这个指针,我们可以像调用普通函数一样调用它。
定义函数指针的语法有点绕:
返回类型 (*指针名)(参数列表);
示例:
// 一个普通的函数
int add(int a, int b) {
return a + b;
}
int main() {
// 定义一个函数指针,它指向一个返回 int、有两个 int 参数的函数
int (*func_ptr)(int, int);
// 将函数 add 的地址赋给这个指针
func_ptr = add;
// 通过指针调用函数
int result = func_ptr(5, 3); // 等同于 add(5, 3);
printf("Result: %d\n", result); // 输出: Result: 8
return 0;
}
func_ptr 本身就是一个变量,它存储了 add 函数的地址。
第 2 步:函数指针的指针
我们把上面的逻辑再推进一步。

func_ptr是一个变量(它存储了函数的地址)。- 我们能不能再创建一个指针,让它指向
func_ptr这个变量呢?
当然可以! 这就是“函数指针的指针”。
它的语法是:
返回类型 (**指针的指针名)(参数列表);
或者为了清晰,可以加上括号:
返回 type (*(*指针的指针名))(参数列表);
示例:
int add(int a, int b) {
return a + b;
}
int subtract(int a, int b) {
return a - b;
}
int main() {
// 1. 定义两个函数指针
int (*func_ptr_add)(int, int) = add;
int (*func_ptr_sub)(int, int) = subtract;
// 2. 定义一个“函数指针的指针”
// 它指向一个类型为 "int (*)(int, int)" 的指针
int (**func_ptr_ptr)(int, int);
// 3. 让这个指针指向 func_ptr_add
func_ptr_ptr = &func_ptr_add;
// 4. 通过函数指针的指针来调用函数
// *func_ptr_ptr 得到它所指向的函数指针 (也就是 func_ptr_add)
// 然后再用 () 来调用这个函数指针
int result1 = (*func_ptr_ptr)(10, 5); // 等同于 add(10, 5);
printf("Add Result: %d\n", result1); // 输出: Add Result: 15
// 5. 改变它指向的函数指针,让它指向 subtract
func_ptr_ptr = &func_ptr_sub;
// 6. 再次调用
int result2 = (*func_ptr_ptr)(10, 5); // 等同于 subtract(10, 5);
printf("Subtract Result: %d\n", result2); // 输出: Subtract Result: 5
return 0;
}

func_ptr_ptr是一个指针。- 它指向的不是一个普通数据,而是一个函数指针。
*func_ptr_ptr的结果是一个函数指针。(*func_ptr_ptr)(...)是在调用这个函数指针所指向的函数。
实际应用场景(iOS 开发)
在 iOS 开发中,你几乎不会自己主动去定义和使用“函数指针的指针”,当你与某些 C 语言库(尤其是 C++ 库)或底层系统 API 交互时,你必须理解它,因为它们会要求你传递这样的参数。
场景 1:回调函数的动态注册
想象一个 C 库,它允许你注册多个回调函数,库的初始化函数可能需要一个“回调函数列表”的指针,这个列表本身就可以用一个“函数指针的指针”数组来表示。
模拟场景:
假设我们有一个 AudioEngine 库,它可以注册多个音效处理函数。
// AudioEngine.h (一个模拟的 C 库头文件)
// 定义一个函数指针类型,方便使用
typedef float (*AudioEffectProcessor)(float input);
// 初始化引擎,并传入一个“处理器指针的指针”和最大数量
void audioEngine_init(AudioEffectProcessor** processors, int max_processors);
// 添加一个处理器到引擎
void audioEngine_addProcessor(AudioEffectProcessor** processors, int* count, AudioEffectProcessor new_processor);
// 运行引擎,处理音频数据
void audioEngine_process(float input);
// main.m (你的 iOS App 代码)
#import <stdio.h>
#import <stdlib.h>
// 引入模拟的 C 库
#import "AudioEngine.h"
// 定义几个具体的音效处理函数
float reverbEffect(float input) {
printf("-> Applying Reverb: %.2f\n", input);
return input * 0.8f; // 简单模拟
}
float distortionEffect(float input) {
printf("-> Applying Distortion: %.2f\n", input);
if (input > 0.5f) return 1.0f;
if (input < -0.5f) return -1.0f;
return input * 2.0f; // 简单模拟
}
int main(int argc, const char * argv[]) {
printf("--- Starting Audio Engine ---\n");
// 1. 准备一个“函数指针的指针”数组
// 这就是库需要的“处理器列表”
AudioEffectProcessor* processor_list = NULL;
// 2. 初始化引擎,传入这个列表的地址
// engine 内部可能会 malloc 一块内存来存储最多 N 个 AudioEffectProcessor*
audioEngine_init(&processor_list, 5);
int processor_count = 0;
// 3. 动态添加处理器
audioEngine_addProcessor(&processor_list, &processor_count, reverbEffect);
audioEngine_addProcessor(&processor_list, &processor_count, distortionEffect);
printf("\n--- Processing Audio ---\n");
// 4. 引擎内部可能会这样遍历并调用所有处理器
for (int i = 0; i < processor_count; i++) {
// processor_list 是一个 AudioEffectProcessor* (函数指针的指针)
// processor_list[i] 是一个 AudioEffectProcessor (函数指针)
// processor_list[i](input) 是调用函数
float output = processor_list[i](0.7f);
printf(" Output from chain: %.2f\n", output);
}
// 5. 清理 (这里省略了 engine 的清理和 free)
free(processor_list); // 假设 engine 内部是 malloc 的
printf("\n--- Engine Stopped ---\n");
return 0;
}
为什么这里要用 `AudioEffectProcessor?** 因为audioEngine_init函数需要**修改**调用者(main函数)中的processor_list变量,它需要分配内存并把内存的地址赋给processor_list,在 C 中,要修改一个变量本身(而不是它指向的内容),必须传递它的地址。processor_list是一个AudioEffectProcessor*,所以它的地址就是AudioEffectProcessor**`。
场景 2:C++ 虚函数表的简化理解
在 C++ 中,虚函数的实现(vtable,虚函数表)可以概念上看作是“函数指针的数组”,当你通过基类指针调用一个虚函数时,编译器会:
- 找到对象开头的指针(指向 vtable)。
- 在 vtable 中查找对应偏移量的函数指针。
- 调用这个函数指针。
这个过程就间接地用到了“指针”(指向 vtable 的指针)和“函数指针”(vtable 中的条目),虽然你不会直接操作它,但理解这个概念有助于你明白为什么某些 C++ API 会需要这种复杂的指针结构。
在 iOS 开发中的具体例子
一个真实世界的例子是 OpenAL,一个跨平台的 3D 音频 API,它使用 C 风格的回调来处理音频数据。
在 ALCcontext 的创建过程中,你可以指定一个 ALCdevice,并且会涉及到各种回调函数的设置,这些回调函数的注册函数,其参数列表中就可能包含类似 void** 或 ALCvoid** 的参数,用于让你传入一个回调函数指针的地址,以便 AL “引擎”可以知道在何时、何地调用你的代码。
虽然 ALCdevice 的创建函数本身不直接是函数指针的指针,但在其更复杂的配置或扩展中,这种模式很常见。
语法速查表
| 概念 | 语法 | 示例 | 解释 |
|---|---|---|---|
| 函数 | 返回类型 函数名(参数列表) |
int foo(int a) |
定义一个名为 foo 的函数。 |
| 函数指针 | 返回类型 (*指针名)(参数列表) |
int (*fp)(int) |
fp 是一个指向函数的指针。 |
| 函数指针的指针 | 返回类型 (**指针的指针名)(参数列表) |
int (**fpp)(int) |
fpp 是一个指向函数指针的指针。 |
| 指向 const 函数的指针 | 返回类型 (*指针名)(参数列表) const (C++), const 返回类型 (*指针名)(参数列表) (C) |
int (*fp)(int) const (C++ style) |
指向一个不能修改其成员的成员函数(C++),在 C 中,const 修饰返回值。 |
| const 函数指针 | 返回类型 (*const 指针名)(参数列表) |
int (*const fp)(int) |
fp 是一个 const 指针,初始化后不能再指向其他函数。 |
| const 函数指针的指针 | 返回类型 (*const *指针的指针名)(参数列表) |
int (*const *fpp)(int) |
fpp 是一个 const 指针,它必须初始化,之后不能再指向其他函数指针。 |
总结与最佳实践
-
是什么? “函数指针的指针”是一个二级指针,它最终指向一个函数,第一级指针指向第二级指针(一个函数指针),第二级指针指向函数的入口地址。
-
为什么用? 主要用于需要修改调用者提供的函数指针变量本身的场景,最常见的用途是让一个库函数能够初始化或填充一个函数指针数组/列表,并将这个列表的地址返回给调用者。
-
在 iOS 开发中如何应对?
-
识别它:当你看到一个函数参数是
void **或者一个特定类型的 时,就要警惕了,它很可能就是一个“指针的指针”。 -
理解意图:调用者需要传递一个
NULL指针或者一个已分配内存的指针地址,库函数会根据这个地址来分配、写入或修改内存。 -
使用
typedef:为了代码清晰,强烈建议使用typedef来为复杂的函数指针类型起一个别名。// 不推荐 void my_lib_init(int (**callback)(char*), int* size); // 推荐 typedef int (*MyCallbackFunc)(char*); void my_lib_init(MyCallbackFunc* callbacks, int* size);
这样,
MyCallbackFunc*的可读性就大大提高了,它清楚地表示“一个指向MyCallbackFunc类型的指针”。
-
-
现代 C++ 的替代方案 在 C++ 中,你会更倾向于使用
std::function和std::vector来实现类似的功能,它们更安全、更易于管理,能避免 C 语言中手动内存管理的复杂性。// C++ 风格 #include <functional> #include <vector> class AudioEngine { public: void addProcessor(std::function<float(float)> processor); void process(float input); private: std::vector<std::function<float(float)>> m_processors; };这种方式比传递
float (**processor)(float)要直观和安全得多,在纯 C++ 项目中,你很少会直接用到函数指针的指针,它更多地是作为与遗留 C 代码或系统 API 交互时的“桥梁”技术。
