目录
- 什么是回调函数
- 回调和直接调用的区别
- 最基础的回调:函数指针
- 为什么函数指针不够用
- 成员函数与成员函数指针
- 仿函数与状态封装
- [Lambda:现代 C++ 的主流回调方式](#Lambda:现代 C++ 的主流回调方式)
- [
std::function与bind](#std::function 与 bind) - 模板回调与性能优化
- 回调函数的实际应用场景
一、什么是回调函数
在 C++ 中,所谓回调函数,可以先用一句话理解:
把一个函数交给另一个函数,在合适的时机再由后者调用,这个被"传进去、再调回来"的函数,就可以看作回调函数。
平时我们写函数,通常是"我主动调用别人";而回调的特点在于:你先把处理逻辑交出去,至于它什么时候执行,不再由你当前代码直接决定,而是由别的模块、对象或框架来调用。
也就是说,回调强调的是一种反过来的调用关系。
类比一个生活中的例子:
你点外卖时给商家留言"送到了给我打电话"。你不是自己主动执行"打电话"这个动作,而是把这个动作交给了商家,等条件满足后由商家来执行。这就很像回调。
二、回调和直接调用的区别
很多人刚学回调时都会觉得:
这不还是函数调用吗?
确实,本质上还是调用函数。
但区别在于:谁决定调用,什么时候调用。
1. 直接调用
直接调用是你自己写下调用语句,你自己决定调用哪个函数、什么时候调用。
cpp
#include <iostream>
using namespace std;
void printHello() {
cout << "Hello" << endl;
}
void test() {
printHello(); // 直接调用
}
2. 回调
回调是你先把函数交给别人,再由别人决定什么时候调用。
cpp
#include <iostream>
using namespace std;
void onFinish() {
cout << "任务完成" << endl;
}
void doTask(void (*cb)()) {
cout << "执行任务中..." << endl;
cb(); // 由 doTask 调用
}
3. 本质区别
可以概括成一句话:
直接调用 :调用权在当前代码手里
回调:调用权先交出去,再由别人调用回来
回调的关键不是"有没有调用函数",而是调用权发生了转移。
三、最基础的回调:函数指针
在 C++ 中,最基础、最原始的回调实现方式,就是函数指针。
前面说过,回调的本质是:
把一个函数交给另一个函数,在之后由对方调用。
那么函数怎么"交给别人"?
答案就是:通过函数指针。
函数指针本质上保存的是函数地址。
既然函数可以通过地址被引用 ,那么我们就可以像传普通参数一样,把函数传给另一个函数。
cpp
#include <iostream>
using namespace std;
using Callback1 = void (*)();
using Callback2 = void (*)(int);
void onFinish() {
cout << "任务完成后的处理逻辑" << endl;
}
void doTask(Callback1 cb) {
cout << "正在执行任务..." << endl;
cb();
}
void handleResult(int x) {
cout << "计算结果是: " << x << endl;
}
int add(int a, int b, Callback2 cb) {
int result = a + b;
cb(result);
return result;
}
int main() {
doTask(onFinish);
add(3, 5, handleResult);
return 0;
}
输出结果:
正在执行任务...
任务完成后的处理逻辑
计算结果是: 8
在这个例子里:
onFinish 和 handleResult 都是普通函数
它们先被传入其他函数,再由其他函数在内部调用,所以它们都可以看作回调函数。
四、为什么函数指针不够用
函数指针能实现最基础的回调,但在真实开发中,它很快就会暴露出局限。原因很简单,现实中的回调,往往不仅仅是"调用一个函数"这么简单。很多时候,我们还希望回调能够:
- 访问上下文
- 绑定对象
- 保存状态
- 根据场景表现出不同逻辑
而这些,正是普通函数指针不擅长的地方。函数指针只能表示一个"裸函数",它能说明返回值类型和参数列表,让我们调用这个函数,但是它不能告诉我们,这个函数属于哪个对象,调用时是否依赖额外数据,以及这个函数是否带有自己的状态 ,所以,函数指针更适合普通函数的简单回调,却不太适合面向对象中的复杂回调。这也是为什么后面 C++ 又提供了更灵活的方式,比如:
- 成员函数指针
- 仿函数
- Lambda
- std::function
五、成员函数与成员函数指针
既然普通函数可以做回调,那么类的成员函数能不能做回调?
答案是:能,但不能直接当普通函数那样用。
原因在于,普通成员函数调用时依赖对象。
它背后隐含着一个 this 指针,所以它并不是一个完全独立的普通函数。
例如:
cpp
#include <iostream>
using namespace std;
class Task {
public:
void onFinish() {
cout << "成员函数处理任务完成" << endl;
}
};
这里的 onFinish 属于 Task 对象。
所以它不能直接赋值给普通函数指针:
cpp
void (*cb)(); // 这是普通函数指针
如果想表示成员函数,需要用成员函数指针:
cpp
void (Task::*cb)() = &Task::onFinish;
调用时还必须绑定对象:
cpp
Task t;
(t.*cb)();
可以看到,成员函数指针虽然解决了"对象方法怎么回调"的问题,但写法比较麻烦。
所以在现代 C++ 中,更常见的做法往往是直接用 Lambda 或 std::function 来包装。
六、仿函数与状态封装
函数指针最大的弱点之一,就是不能保存状态。
而仿函数正好解决了这个问题。
所谓仿函数,本质上就是:重载了 operator() 的对象。
cpp
#include <iostream>
#include <string>
using namespace std;
class Printer {
public:
Printer(string msg) : msg_(msg) {}
void operator()() const {
cout << msg_ << endl;
}
private:
string msg_;
};
使用时:
cpp
Printer p("任务完成");
p();
这里的 p 看起来像函数,但它其实是对象。
相比普通函数,仿函数的优势在于:它可以把数据和行为绑定在一起。
这就意味着,回调不再只是一个"裸函数",而可以是对象 + 状态的组合。
七、Lambda:现代 C++ 的主流回调方式
在现代 C++ 中,最常用的回调写法通常是 Lambda。
Lambda 的优点很明显:
- 写法短
- 就地定义
- 能捕获上下文
- 很适合临时回调
例如:
cpp
#include <functional>
#include <iostream>
#include <string>
using namespace std;
void doTask(const function<void()>& cb) {
cout << "正在执行任务..." << endl;
cb();
}
int main() {
string msg = "任务完成";
doTask([msg]() {
cout << msg << endl;
});
return 0;
}
这里的[msg]表示把外部变量msg捕获进来。
这也是 Lambda 最强的地方:它既像函数,又能带状态。
所以从工程角度看,Lambda 基本就是现代 C++ 回调的主流写法。
八、std::function 与 bind
如果说 Lambda 解决的是"怎么方便地写回调",
那么 std::function 解决的就是"怎么统一地接收回调"。
它可以统一保存各种可调用对象,比如:
- 普通函数
- 成员函数包装结果
- 仿函数
- Lambda
例如:
cpp
#include <functional>
#include <iostream>
using namespace std;
void run(const function<void()>& cb) {
cb();
}
这样 run 就不必关心你传进来的是哪一种回调形式。
bind
bind 的作用,是把函数和参数提前绑定起来,变成一个新的可调用对象。
cpp
#include <functional>
using namespace std;
class Task {
public:
void finish() {
cout << "finish" << endl;
}
};
Task t;
auto f = bind(&Task::finish, &t);
f();
不过现在很多场景下,Lambda 往往比 bind 更直观。
所以实际开发中,一般更推荐优先写 Lambda。
九、模板回调与性能优化
虽然 std::function 很方便,但它是一个比较通用的封装。
如果你更关心性能,有时会直接用模板来接收回调。
cpp
template <typename Callback>
void doTask(Callback cb) {
cout << "正在执行任务..." << endl;
cb();
}
这种写法的优点是:
- 更灵活
- 可能更容易内联
- 少一层通用封装
缺点是接口会更"模板化",可读性稍差一点。
所以可以简单理解为:
想要接口统一、写法方便:用 std::function
想要更高性能:考虑模板回调
十、回调函数的实际应用场景
回调并不是一个只存在于教材里的概念,它在工程里非常常见。
-
事件处理
比如按钮点击、鼠标输入、消息到达。
本质上都是"事件发生后执行某段逻辑"。
-
异步编程
比如网络请求完成后通知、线程任务执行结束后处理结果。
这种场景天然适合回调。
-
STL 算法
STL 里很多算法都带有回调思想。
例如比较器、本质上就是把"比较规则"交给外部决定。
cpp
sort(v.begin(), v.end(), [](int a, int b) {
return a > b;
});
- 解耦设计
把流程控制和具体行为拆开,让代码更容易扩展。
这也是回调最核心的工程价值之一。