回调函数(Callback)

回调函数(Callback)

一、一句话定义

回调函数是"你写好、但不由你调用,而是交给别人在合适时机调用"的函数。

它的关键不在"函数"三个字,而在调用权的转移:你把函数的地址(或可调用对象)作为参数交给另一个模块,双方约定"当某件事发生时,请调用这个函数"。

二、和普通调用的区别

普通调用 回调
谁发起调用 你的代码 被调用的那个模块(框架/库/系统)
调用时机 你写在哪一行就何时执行 由外部事件决定,你无法预知
数据流方向 你的代码 → 库 库 → 你的代码
控制权 始终在你手里 交给框架,发生"控制反转"

普通调用是"我调库":

c 复制代码
int n = strlen("hello");

回调是"库调我":

c 复制代码
int cmp(const void *a, const void *b) { ... }
qsort(arr, 10, sizeof(int), cmp);   // cmp 由 qsort 在排序过程中反复调用

qsort 完全不需要知道你要比较什么类型、按什么规则排序,它只负责排序算法本身,比较规则由你通过回调注入。这就是回调最原始、最典型的价值。

三、生活中的类比

  • 订闹钟:你设好"7 点叫我",然后去睡觉。叫醒动作不是你执行的,是闹钟在条件满足时替你执行。你设置的就是一个回调。
  • 点外卖:你留下地址和电话,餐做好了商家/骑手会主动联系你,而不是你一直打电话问"好了吗"。留联系方式 = 注册回调,被联系 = 回调被触发。
  • 医院挂号后等叫号:你不需要每隔几分钟去问护士,系统到号会喊你。

共同点:你留下"到时候要做的事",然后就不管了,由对方在正确时机执行。

四、C 语言里的回调:函数指针

C 没有闭包和 lambda,回调就是函数指针。

c 复制代码
/* 定义回调类型:接收 int,返回 void */
typedef void (*event_handler_t)(int event_id);

/* 库函数:只负责"何时调用",不关心"调用后干什么" */
void run_event_loop(event_handler_t handler, int loops)
{
    for (int i = 0; i < loops; ++i) {
        /* 模拟事件发生 */
        handler(i);            /* 反向调用你注册的函数 */
    }
}

/* 你写的回调 */
void on_event(int event_id)
{
    printf("event %d happened\n", event_id);
}

int main(void)
{
    run_event_loop(on_event, 3);   /* 注册,把函数地址交出去 */
    return 0;
}

系统 API 里回调无处不在:

  • pthread_create(&tid, NULL, start_routine, arg):start_routine 是新线程的回调,pthread 库负责起线程,然后调用它;
  • signal(SIGINT, handler):信号到达时由内核回调你的 handler;
  • atexit(cleanup):进程退出前由运行时回调;
  • qsort / bsearch 的比较函数。

五、需要携带上下文时:C 的 void* 惯用法

C 的函数指针无法携带额外数据,标准做法是给回调加一个 void *userdata 参数,把上下文"塞"进去:

c 复制代码
typedef void (*handler_t)(int event_id, void *userdata);

void run_loop(handler_t handler, void *userdata)
{
    for (int i = 0; i < 3; ++i)
        handler(i, userdata);
}

struct Context {
    const char *name;
    int count;
};

void on_event(int id, void *userdata)
{
    struct Context *ctx = (struct Context *)userdata;
    ctx->count++;
    printf("[%s] event %d, total %d\n", ctx->name, id, ctx->count);
}

int main(void)
{
    struct Context ctx = { "worker", 0 };
    run_loop(on_event, &ctx);
    return 0;
}

这个 void *userdata 模式在 C 库(如 libevent、SQLite、各种驱动框架)里极其常见。C++ 的 std::bind / lambda 捕获,本质上就是在语言层面替你做这件事。

六、C++ 里的回调:四种写法

1. 函数指针(最简单,无状态)

cpp 复制代码
void onTick(int n) { std::cout << "tick " << n << "\n"; }
void run(void (*cb)(int)) { cb(1); }
run(onTick);

2. 成员函数指针 + 对象(回到 C 风格,需要手动带 this)

cpp 复制代码
class Timer {
public:
    void onTick(int n) { std::cout << "Timer::onTick " << n << "\n"; }
};

成员函数不能像普通函数那样直接取地址调用,必须同时知道"哪个对象",所以实际工程里会配合 std::bind 或 lambda。

3. std::function + std::bind

cpp 复制代码
#include <functional>

class Timer {
public:
    void onTick(int n) { std::cout << "tick " << n << "\n"; }
};

void run(const std::function<void(int)> &cb) { cb(42); }

Timer t;
run(std::bind(&Timer::onTick, &t, std::placeholders::_1));  // 绑定对象 + 占位符

std::bind(&Timer::onTick, &t, _1) 的含义是:把成员函数 Timer::onTick 和对象 t 打包成一个普通的 void(int) 可调用对象。这样库只认 std::function,不需要认识你的类。

4. Lambda(现代 C++ 首选)

cpp 复制代码
int counter = 0;
run([&counter](int n) {
    counter += n;                 // 捕获外部变量,等价于携带 userdata
    std::cout << "counter=" << counter << "\n";
});

Lambda 的捕获列表([&]、[=]、[this])就是"上下文携带"的语法糖,比 void* 和 bind 都清晰。

注意:[&] 按引用捕获,若 lambda 的生命周期长于被捕获变量(比如被存起来稍后执行),会产生悬垂引用。这是回调最常见的 bug 来源之一。

七、回调的作用

  1. 解耦

    框架/库只定义"插槽"(函数签名),不依赖具体业务;你不需要修改库源码就能定制行为。库的作者和业务作者可以互不知晓地工作。

  2. 事件驱动 / 异步

    事件何时发生不可预知。你不可能"死等"客户端连接或用户点击,只能先注册处理函数,事件到来时被调用。所有 GUI 框架、网络库、Node.js 风格异步 API 都建立在这个模式上。

  3. 控制反转(IoC)

    程序主体交给框架,你只填"空位"。这也是很多框架让人初期不适应的原因:main 很快就"结束"了,真正的逻辑都在等待被调用。

  4. 策略注入 / 可扩展

    排序规则、比较函数、日志输出目标、超时处理策略......同一套流程用不同回调就能产生不同行为,避免写一堆 if/else 分支或继承子类。

八、代价与常见坑

问题 说明
控制流不直观 代码不再从上到下一眼读完,调试时需要顺着"谁注册的、谁触发的"追
生命周期问题 回调里引用了已销毁的对象(悬垂 this、悬垂引用)会崩溃或数据错乱
线程安全 回调可能在别的线程执行,共享数据必须加锁或通过消息投递串行化
重入 / 递归 回调中再次触发同一事件,可能造成无限递归或状态不一致
异常传播 C 风格回调里抛出异常会跨过 C 栈帧,属于未定义行为
回调地狱 层层嵌套的异步回调难以阅读,现代方案是 Promise / Future / 协程

避免悬垂的关键原则 :谁注册回调,谁就要负责保证"回调被触发时,它引用的对象还活着"。常见做法是注册时使用弱引用(std::weak_ptr),或在对象析构时主动注销回调。

九、同步回调与异步回调

  • 同步回调 :注册后立即(或在本函数返回前)被调用,例如 qsort 的比较函数、std::for_each 的仿函数。调用栈连续,行为容易预测。
  • 异步回调 :注册后先返回,未来某个时刻在别处(可能是别的线程)被调用,例如 pthread_create 的入口函数、网络库的消息回调、setTimeout。

判断方式:回调执行时,注册它的那个函数是否已经返回。异步回调才需要考虑生命周期和线程安全。

十、总结

  • 回调 = 把函数当参数传出去,把调用权交给对方。
  • 本质是"延迟执行 + 解耦":现在不知道什么时候做、由谁触发,所以先把"要做什么"存下来。
  • C 用函数指针 + void*,C++ 用 std::function / std::bind / lambda,思想完全一致。
  • 用它的收益是解耦和事件驱动能力,代价是控制流变隐晦,以及必须谨慎处理生命周期与线程安全。
相关推荐
秋名RG1 小时前
算法的划分
算法
less_121381 小时前
鸿蒙 WPS Open SDK 二开复盘:从注册、打开到回传的完整能力链
服务器·sdk·harmonyos·wps·鸿蒙开发
木泽八1 小时前
《Ubuntu系统GRUB引导修复操作记录》
linux·运维·ubuntu
Tisfy1 小时前
LeetCode 3619.总价值可以被 K 整除的岛屿数目:广度优先搜索BFS
算法·leetcode·bfs·宽度优先
地平线开发者1 小时前
【大模型】EAGLE 与 EAGLE-3:用“特征预测“加速投机解码
算法
水饺编程2 小时前
第2章:Linux 基础,删除用户
linux·c语言·汇编
Novproxy_Z2 小时前
机房 IP 和住宅 IP 怎么区分?IP ASN、归属信息检测脚本
linux·网络·tcp/ip
lengxuenong2 小时前
区间覆盖问题 · 简明学习手册1
java·学习·算法
明志数科2 小时前
人类视频与机器人示教的数据配比:分层训练的数据采购与采集设计
人工智能·算法