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(阻塞等待代替轮询)

更多推荐