C++ 回调函数搞懂指南
·
文章目录
C++ 回调函数完全搞懂指南
一句话说清楚
回调函数就是你写好的代码,交给别人保管,等事情发生时别人帮你调用。你失去了"什么时候执行"的掌控权,但换来了"不用在那干等"的自由。
一、从一个生活例子开始
你去楼下快递柜寄快递:
正常做法(同步,自己盯着):
你把快递放进柜子 → 站在柜子前面等快递员来取 → 快递员来了 → 你看着他取走 → 回家
问题:你傻站了半小时,什么都没干
回调做法(异步,留个电话):
你把快递放进柜子 → 在系统里留个回调:"快递被取走时发短信通知我" → 立刻回家打游戏
→ 半小时后手机响了:"您的快递已被取走" → 你知道完事了
好处:你不用盯着,该干嘛干嘛
你留在系统里的那个"发短信通知我",就是回调函数。
二、不用回调 vs 用回调
2.1 不用回调:死循环盯着
// 查 ODU 设备温度
double 查温度() {
while (true) {
发查询命令();
sleep(10ms);
if (设备回复了()) {
return 设备回复的数据;
}
// 没回复?继续循环等着
}
}
// 调用
double temp = 查温度(); // 这行要卡 800ms,期间什么都干不了
处理温度(temp);
致命问题: 线程被卡住,这 800ms 内不能处理任何其他事情。
2.2 用回调:发完请求立刻返回,结果到了自动处理
// 查 ODU 设备温度(异步)
void 查温度(std::function<void(double)> 回调) {
发查询命令();
把回调存起来; // 不执行,只是记下来
}
// 这一行立刻返回!不卡!
// 另一个线程收到设备回复时:
void 收到回复(double value) {
所有的回调(value); // 现在才执行
}
// 调用
查温度([](double temp) {
处理温度(temp); // 这段代码 800ms 后才执行,但调用者不用等
});
做其他事情(); // 这行立刻执行,不卡
三、回调的核心三要素
| 要素 | 代码 | 生活类比 |
|---|---|---|
| 注册 | setCallback(函数) |
给快递公司留电话 |
| 存储 | callback_ = 函数 |
快递公司记下你的号码 |
| 触发 | callback_(参数) |
包裹到了,快递员打你电话 |
// 最小例子:就三样东西
std::function<void(int)> callback; // ① 准备一个能装函数的变量
callback = [](int x) { cout << x; }; // ② 把函数装进去(此刻不执行)
callback(42); // ③ 调用它(此刻才执行)
四、我的项目中回调出现的每一处
4.1 HTTP 路由——浏览器访问时触发
// webd/api_odu.cpp
server.Get("/api/v1/odu/monitor", [&ipc](req, res) {
// ↑ 这个 lambda 就是回调
// 浏览器访问这个路径时,httplib 框架才调用它
writeJson(res, 200, 查询ODU数据());
});
4.2 IPC 请求处理器——收到消息时触发
// odud/main.cpp
ipc.setRequestHandler([&manager](const IpcMessage& req) {
manager.handleRequest(req);
});
// 回调存进 request_handler_ 变量里
// rxLoop 线程收到 request 消息时调用
// 触发处:ipc_client.hpp rxLoop()
if (msg.type == "request") {
request_handler_(msg); // ← 调回调
}
4.3 promise/future——异步等待结果
// ipc_client.hpp request()
auto future = promise.get_future();
waiters_.emplace(seq, std::move(promise)); // 回调 = promise.set_value()
send(msg); // 发请求
return future.get(); // 阻塞等,直到 rxLoop 调回调
// 触发处:rxLoop 收到 response
it->second.set_value(msg); // ← 调"回调"(promise 的 set_value)
4.4 串口任务完成 → 批次聚合
// odu_request_manager.cpp
// 每个串口任务完成后调 finishBatchField
// 最后一个任务调 finishBatchField 时会触发 ipc_.respond() —— 这个就是"最终回调"
4.5 并行查询多模块
// webd/api_odu.cpp
auto odu = std::async(std::launch::async, [&] {
return requestData(ipc, "odud", "odu.get_monitor", {});
});
// 开线程,线程里执行回调
五、Qt 信号槽也是回调
我在 Qt 里写的这些,本质都是回调:
// Qt C++
connect(button, &QPushButton::clicked, this, &MyClass::onClicked);
// onClikcked 就是回调,存进 Qt 元对象系统,鼠标点击时调用
// QML
Button {
onClicked: { console.log("点了") }
// 这个 {} 就是回调,Qt 替你存好了
}
| Qt 信号槽 | C++ 原生回调 | |
|---|---|---|
| 存储 | Qt 元对象系统 | std::function 变量 |
| 注册 | connect() / onXxx: {} |
setHandler(lambda) |
| 触发 | emit signal() |
callback_(args) |
| 依赖 | 必须 QObject + moc | 纯 C++ 标准库,零依赖 |
| 一对多 | ✅ | ❌(要自己实现) |
| 跨线程 | ✅ | ❌(要自己处理) |
Qt 信号槽 = 加强版回调。嵌入式 Linux 没有 Qt,只能用标准库的 std::function。
六、为什么不用死循环轮询
// 轮询方式
while (!消息到了) {
sleep(1ms); // CPU 空转,浪费电
}
处理消息();
// 回调方式
收消息线程 {
recv(); // 阻塞等待,没消息就睡觉,CPU 占用 0%
回调(消息); // 有消息才被唤醒
}
| 轮询 | 回调 | |
|---|---|---|
| CPU 占用 | 一直占着 | 0%(阻塞等待) |
| 响应速度 | 取决于轮询间隔 | 来即响应 |
| 代码耦合 | 框架和业务粘死 | 框架只管调回调,不管内容 |
七、回调的注意事项
7.1 引用捕获的生命周期
// ❌ 危险
void 设置回调() {
Manager manager;
ipc.setRequestHandler([&manager](msg) { // 按引用捕获
manager.handleRequest(msg);
});
} // manager 析构了!但回调还在 → 悬垂引用 → 崩溃
// ✅ 安全(本项目做法)
int main() {
Manager manager; // manager 在 main 栈上
ipc.setRequestHandler([&manager](msg) { // 按引用捕获
manager.handleRequest(msg);
});
while(true) sleep(24h); // main 不退出,manager 一直活着
}
规则:引用捕获时,被捕获对象必须比回调活得久。
7.2 回调里不要做耗时操作
// ❌ 危险
ipc.setRequestHandler([](msg) {
发串口请求并等 800ms(); // 卡住 rxLoop 线程!其他消息收不到了
});
// ✅ 正确(本项目做法)
ipc.setRequestHandler([](msg) {
把任务扔进队列(); // 几微秒,立刻返回
// 耗时操作在别的线程里做
});
八、常见应用场景汇总
| 场景 | 例子 |
|---|---|
| 网络/串口 I/O | 发请求 → 等回复 → 收到时调回调 |
| UI 事件 | 点按钮 → 调回调 |
| 定时器 | 设一个定时器,“5 秒后调这个回调” |
| 异步计算 | 提交任务,“算完了调这个回调” |
| 库/框架扩展 | 框架写好流程,用户填回调(std::sort 的比较函数) |
| 中断处理 | 硬件中断 → 调 ISR 回调 |
| 多结果聚合 | 每个结果到了调回调,足够数量时触发最终处理 |
九、一句话记忆法
正常调用 = 你去敲门。回调 = 你留电话号码,别人打给你。
- 正常:你知道什么时候调,你掌控时机
- 回调:你写好逻辑交给别人,别人掌控时机
回调不是为了炫技,是为了:
- 不卡线程(异步)
- 解耦框架和业务(一份框架代码多处复用)
- 省 CPU(阻塞等待代替轮询)
更多推荐



所有评论(0)