C++ 函数指针、回调与 Lambda 全解析
在学习 C++ 的过程中,函数指针常常让人觉得“古老又晦涩”。但其实它的思想非常简单:函数本身就是一块内存中的指令序列,我们完全可以把函数的地址当作变量传来传去。
这样,我们就可以在运行时决定到底调用哪个函数,从而让代码更灵活。本文会结合实例代码,从函数指针讲到 Lambda 和 std::function
,最后总结适用场景。
1. 函数指针是什么?
我们平时调用函数是这样的:
void HelloWorld() {std::cout << "Hello World!" << std::endl;
}int main() {HelloWorld(); // 直接调用
}
但如果我们写成:
int main() {auto myHelloWorld = &HelloWorld; // 获取函数地址myHelloWorld(); // 通过函数指针调用
}
依然会输出:
Hello World!
这里 auto
推导出来的类型是 void(*)()
,意思是:
一个指向“无参数、返回 void 的函数”的指针。
也就是说,函数名本身就能转化为函数指针。如果你写 HelloWorld
(不带括号),那就是函数地址;如果写 HelloWorld()
,那就是调用函数。
等价写法如下:
// 方法1:用 auto(推荐)
auto myHelloWorld = HelloWorld;// 方法2:显式声明
void (*myHelloWorld)() = HelloWorld;// (最佳)方法3
using myFunctionType = void(*)();
myFunctionType myHelloWorld = HelloWorld;// 方法4
typedef void(*myFunctionPtr)();
myFunctionPtr myHelloWorld = HelloWorld;
2. 带参数的函数指针
如果函数有参数,那函数指针的声明方式就是:
*返回类型 (变量名)(参数类型...)
void PrintValue(int value) {std::cout << "Value: " << value << std::endl;
}int main() {void (*func)(int) = PrintValue; // 声明函数指针func(42); // 调用
}
输出:
Value: 42
这说明函数指针和普通函数几乎一样,只是多了“把函数当变量”的能力。
3. 为什么要用函数指针?
如果只是一个函数调用另一个函数,我们完全没必要用函数指针。
它的价值在于:让函数能接收另一个函数作为参数,从而实现回调、算法选择、框架抽象。
示例1:通用循环处理(回调)
假设我们希望对数组里的每个元素都执行某个操作:
void PrintValue(int value)
{std::cout << "Value: " << value <<std::endl;
}void ForEach(const std::vector<int>& values, void(*func)(int))
// 希望在这个函数里调用某个函数 本例中将会调用PrintValue
{for (int value : values){func(value);}
}int main()
{std::vector<int> values = { 1, 5, 2, 4, 3};ForEach(values, PrintValue);// 传入了名为values的vector// 然后对这个vector中的每一个元素 都执行PrintValue函数std::cin.get();
}
好处是:ForEach
本身不关心“怎么处理元素”,只管循环。
真正的处理逻辑交给调用者决定,这就是回调的思想。
示例2:运行时选择算法
如果我们需要在运行时决定“用哪种处理方式”,函数指针也能派上用场:
void ProcessMode1(int x) { std::cout << "Mode1: " << x << "\n"; }
void ProcessMode2(int x) { std::cout << "Mode2: " << x << "\n"; }/*void (*processor)(int) 声明了一个 函数指针,
它可以指向任何接收 int 参数、返回 void 的函数。初始值设为 nullptr,表示当前还没有选定处理函数。*/
void ProcessData(int mode, const std::vector<int>& data) {void (*processor)(int) = nullptr;if (mode == 1) processor = &ProcessMode1;else if (mode == 2) processor = &ProcessMode2;for (int v : data) processor(v);
}
这样就避免了在每个 if-else
分支里重复写循环代码。
4. 回调机制(事件驱动编程)
函数指针的一个经典用途是 事件回调:我们把一个函数交给某个对象,等特定事件发生时,它再调用这个函数。
#include <iostream>// 定义一个 Button 类,模拟按钮
class Button {private:// 成员变量,保存一个函数指针// 类型是 "void (*)()",即指向一个无参无返回值的函数void (*onClickHandler)() = nullptr; // 默认初始化为空指针(表示没有注册回调)public:// 注册点击事件回调函数// 参数是一个函数指针:指向 "void func()" 这种无参数、无返回值的函数void setOnClick(void (*callback)()) {onClickHandler = callback; // 保存用户传进来的函数指针}// 模拟按钮被点击void click() {if (onClickHandler) // 判断是否已经注册了回调onClickHandler(); // 调用回调函数}};// 一个普通函数,符合回调要求:无参数、无返回值
void saveFile() {std::cout << "Saving...\n";
}int main() {Button saveBtn; // 创建一个按钮对象saveBtn.setOnClick(&saveFile); // 注册回调,把 saveFile 绑定到按钮点击事件// 这里传递的是函数指针 "&saveFile",其实也可以直接写 saveFile(C++ 会自动转为指针)saveBtn.click(); // 模拟用户点击按钮,触发回调,执行 saveFile()
}
这里 Button
类完全不知道“点击后做什么”,只管触发 onClickHandler
。
这样就达到了 解耦:按钮不依赖于具体的保存逻辑。
5. Lambda 与 std::function
在上一节里我们用过 传统函数指针(void(*)(int)
),它确实能传函数,但有几个缺点:
不能捕获外部变量
比如想在回调里用外面的局部变量,函数指针就无能为力。语法不直观
声明函数指针的语法比较复杂,不如现代 C++ 的写法清晰。只能绑定函数
没办法直接传 lambda 或仿函数对象。
于是,C++11 引入了 Lambda 表达式 和 std::function
,让回调写法更灵活优雅。
Lambda 表达式
Lambda 就是一个匿名函数,写法是:
[capture](parameters) -> return_type {// 函数体
}
capture
:捕获外部变量的方式(值捕获、引用捕获等)。parameters
:参数列表。return_type
:返回值类型(大多数情况可以省略)。
例子:
int a = 10;// 定义一个 Lambda:参数 v,返回 a+v
auto func = [&a](int v) { return a + v; };std::cout << func(5); // 输出 15
std::function
std::function
是一个通用的 函数包装器,它能存储:
普通函数
函数指针
Lambda 表达式
仿函数对象(重载
operator()
的类)
#include <functional>void ForEach(const std::vector<int>& values, std::function<void(int)> func) {for (int v : values) func(v);
}int main() {std::vector<int> values = {1, 2, 3};// 直接写匿名函数ForEach(values, [](int v) { std::cout << v << "\n"; });// 捕获外部变量int a = 10;ForEach(values, [&a](int v) { std::cout << a + v << "\n"; });
}
和 void(*)(int)
不同,std::function<void(int)>
可以接受:
普通函数
Lambda(捕获变量)
仿函数对象
这让它更适合现代 C++ 编程。
6. 小结
函数指针:就是保存函数地址的变量,能把函数当作参数传递。
主要用途:动态选择算法、事件回调、插件系统、通用算法框架。
现代替代方案:
std::function
+ Lambda,更灵活,也能捕获外部变量。核心思想:抽取变化部分(用函数指针/回调),统一框架部分,避免重复代码。