回调函数

回调函数
小米里的大麦回调函数
1. 认识回调函数?
1. 什么是回调函数?
回调函数(Callback Function)是 函数指针的一种应用形式,它的核心思想是:“把函数作为参数传给另一个函数,由那个函数在合适的时机再调用它。” 通常用于 “别人调用你自己的函数”,你把函数地址传给某个函数,等它内部逻辑执行到某个阶段时,再“反过来”调用你传入的函数。换句话说:
- 你写函数 A(定义了“要做什么”)。
- 别人写函数 B(控制“什么时候做”)。
- 你把 A 的函数地址交给 B,B 内部“回头”去调用 A。这就是“回调”。
如果你还是不理解,那么我先在这里埋一个伏笔:对于我来说,回调函数虽然从英文翻译过来回调不太好理解,但是用过函数传参就能直接上手回调函数,它的用法就是将函数作为参数进行传递,传到对方函数中,类比变量一样被调用,这!就是回调函数,还是很好理解吧,后面来几个示例就能深入理解和上手啦。
2. 使用场景
回调函数常用于以下几种情况:
- 事件驱动:如网络库 muduo 等库中,收到数据时触发回调函数。
- 异步编程:任务完成后调用回调报告结果。
- 通用算法:如
qsort()让用户自定义排序比较规则。 - 钩子机制:插件式扩展、信号槽系统。
| 场景示例 | 回调的含义 |
|---|---|
| GUI 按钮点击事件 | 用户点击按钮后,系统调用你的“回调函数”处理事件 |
| 网络 I/O | 有数据到达时,框架调用注册好的“onMessage”函数 |
| 排序算法 | 排序逻辑由库提供,比较规则由你提供的“比较回调函数”定义 |
当然,回调函数的使用场景远不止于此!
2. 回调函数的使用
1. 函数指针定义
1 | 返回类型 (*函数指针名)(参数列表); |
2. 基本使用(C 风格)
1. 代码示例 1
1 | // 无参无值的回调函数: |
在这个代码中,print 函数就是回调函数,它被当作参数在 test 函数中进行传递,注意传参的写法:返回值 (*函数指针)(参数列表),只是这里无参无返回值,然后回调函数 print 在 for 循环中的 callback(); 被调用。
2. 代码示例 2
1 | // 有参有返回值的回调函数(add、sub、mul、divi 都是回调函数) |
在这个代码中,加减乘除 4 个函数都是回调函数,在 calculate 函数中当作参数进行传递,注意此时传参写法:int (*callback)(int x, int y),指明了返回值、函数指针及其参数,由 callback(a, b) 执行回调逻辑,即每个回调函数自己内部的代码逻辑。
上面的代码虽然简单,但是也差不多能看出来回调函数用处的强大,回调函数有着很多高级用法,本文暂不涉及,用的多了,和其他语法、库、容器等搭配使用才能领略回调的强大!
3. 取别名
不知道大家初看上面的代码感觉咋样,反正我觉得不那么好看/好辩认,所以通常会用到取别名,回调函数取别名的方法主要用 typede 和 using(C++11 及其往后),先介绍一下 typedef 和回调函数搭配的语法:
1 | typedef 返回值类型 (*类型名)(参数列表); |
1. typedef 为回调函数取别名
1 |
|
如果是初学者可能会对这个 typedef 的用法比较疑惑,没关系,我来解答:typedef 的语法本质是 “先写一个该类型的变量声明,再把变量名换成新类型名”。typedef 旧名字 新名字 这种用法应该比较常见的,但这种用法仅只适用于 简单类型(比如 typedef int MyInt;),但对于 复杂类型,比如指针、数组、函数指针就会存在一点区别:
- 简单类型: 想给
int起个别名MyInt会用到typedef int MyInt,此时MyInt是int的别名。 - 指针类型: 想给
int起个别名IntPtr会用到typedef int* IntPtr,此时IntPtr是int的别名。 - 函数指针类型: 想给 指向
void (int)函数的指针 起个别名callback: 先写 定义一个这种指针的变量 cb:void (*cb)(int);,这里的 cb 是一个指针,指向 参数为 int、返回 void 的函数,加 typedef,把变量名 cb 换成新类型名 callback 就得到了typedef void (* callback)(int);,此时callback是void (*)(int)这个函数指针类型的别名。 - ……(更多复杂类型)
void (*callback)(int) 它表示:callback 是一个 指向函数的指针,该函数 返回类型为 void,并且 参数列表为 (int)。可以用自然语言读成:callback 是一个指向接受一个 int 参数、返回 void 的函数的指针。所以,当你再次遇到它样子变种时,知道怎么看了吧。
注意括号的作用:
void *callback(int)是完全不同的意思(它表示“返回void*的函数”),只有加括号void (*callback)(int)才表示“指向函数的指针”!
2. using 为回调函数取别名
语法格式(C++11 及其以后):
1 | using 别名 = 返回值类型 (*)(参数列表); |
1 |
|
我觉得这里应该什么疑问吧,按照格式套就行。
3. 模板支持(为什么推荐使用 using?)
先来看看下面 2 个代码:
1 | // 代码1: |
相信看完上面的代码,在回调函数和模板同时存在时,你再也不想用 typedef 了,还是 using 来的香,当遇到更复杂的场景,你第一个想到的是 typedef 还是 using 呢?言归正传,即使在非回调函数的场景中,我们始终推荐使用 using!
4. std::function —— 通用的函数包装器
C++11 及以后标准中,用 std::function、std::bind、Lambda 等来封装、传递和调用回调函数。注意:它能存储:普通函数、Lambda 表达式、成员函数、仿函数对象等,它让“函数”也能像普通变量一样传来传去,用于回调最方便,这一点在后面会遇到。
1. 基本语法结构
1 |
|
示例:
1 | function<void(int)> cb; // 可以存储任何“接收int、返回void”的可调用对象 |
2. 代码示例 1(入门)
1 |
|
如果上面传统 C 风格的回调函数会用了,那么这里了解完 std::function 的语法格式,代码其实很浅显易懂了,锻炼一下不要注释能不能理解,真不是我懒得不想写注释 🤪。
3. 代码示例 2(进阶)
1 | // 示例1: |
这里就推荐以后直接用 using 了,和 std::function 搭配的语法格式:using 别名 = std::function<返回值类型(参数类型...)>; 需要注意一下。 示例 3 的代码可能需要细细品味一下……
4. std::bind 和占位符的使用
std::bind 的本质是:把一个函数和它的部分参数“提前绑定”起来,生成一个新的可调用对象,其语法结构是:
1 | auto 新函数 = bind(函数地址, 参数1, 参数2, ...); |
代码示例:
1 |
|
占位符 _1、_2 的意义:std::bind 可以“延后传入”某些参数。
1 | auto f = bind(show, _2, _1); |
换句话说,_1, _2 只是告诉编译器:以后当我调用这个函数时,把第 1 个、2 个实参放到对应位置。这似乎很简单,没什么难的吧,下面要介绍 2 个注意点:bind 的本质是 “提前绑定部分参数,生成一个新的函数对象”,这个新函数对象的参数数量是固定的 —— 等于你在 bind 时使用的占位符个数 ,所以:bind 生成的函数对象必须在一次调用中补全所有未绑定的占位符,无法分多条语句逐个补充,只要有占位符未被赋值,就无法触发函数执行。
- 只能用一条语句全部补充: 意思是
bind中存在占位符导致参数“不完整”,后续 只能用一条语句全部补充完整! - 全部补充完毕才会执行回调/只要有缺失就永远不会回调: 意思是只有当
bind的参数真正完整的那一条语句才会执行回调函数,如果不完整,参数缺失(占位符没被填补),那么回调函数就永远不会生效、永远不会被调用!
再来看一小段代码巩固一下吧:
1 |
|
5. bind 的返回值是什么?
std::bind 会返回一个 可调用对象,这个对象的类型是编译器自动生成的匿名类型(类似 lambda 表达式的类型),因此无法直接用具体的类型名声明(比如不能写成 std::bind_type f = ...)。
1. 万能 auto
auto 是个好东西,我不关心,直接就无脑 auto 就行了:
1 | void func(int a, int b) {} |
2. 显式指定类型的方法:用 std::function 包装
虽然 bind 的返回值类型匿名,但可以根据 原函数的签名和绑定后的参数要求,用 std::function 显式指定类型。具体规则是:std::function 的模板参数 = 绑定后需要传入的参数类型 + 返回值类型。
示例 1:绑定后需要传入 1 个参数,返回值为 void。
1 |
|
示例 2:绑定后不需要传入参数,返回值为 int。
1 | int add(int a, int b) |
示例 3:绑定后需要传入 2 个参数,返回值为 bool
1 | bool compare(int a, int b, int c) |
3. 如何确定 std::function 的类型?
只需明确一个问题:调用 bind 生成的函数对象时,需要传入几个参数?每个参数的类型是什么?返回值类型是什么?
步骤拆解:
- 确定原函数的返回值类型(比如
void、int、bool)。 - 统计
bind中使用的占位符数量(_1、_2…),这就是调用时需要传入的参数个数。 - 占位符的类型对应原函数中未被固定的参数类型(比如
_1对应原函数中第一个未绑定的参数类型)。 - 组合上述信息,得到
std::function<返回值类型(参数类型1, 参数类型2...)>。
似乎来的不如
auto香,所以实际怎么写,不用我多说了吧 🤪。
6. 什么时候必须显式指定类型?
作为类的成员变量:类成员变量不能用
auto声明,必须显式指定类型。1
2
3
4
5
6
7
8
9
10
11class MyClass
{
private:
// 显式指定bind返回值的类型为function<void(int)>
function<void(int)> callback;
public:
void setCallback()
{
callback = bind(show, 100, placeholders::_1);
}
};作为函数的返回值:函数返回值不能用
auto(C++14 起允许,但显式类型更清晰)。1
2
3
4
5// 函数返回一个"需要传入int、返回void"的可调用对象
function<void(int)> getCallback()
{
return bind(show, 100, placeholders::_1);
}
5. 扩展
1. std::ref / std::cref —— 引用包装器
bind、function 默认会“拷贝”参数。若想 传引用(例如防止对象被拷贝),要用 ref()。
1 |
|
若不用 ref(),bind 会拷贝一份 a,打印永远是 5。此外,ref() 比较简单就不多说了,直接当 & 这个引用用就行,其他章节也会有涉及。
2. std::mem_fn —— 将“成员函数指针”转成可调用对象
有时我们希望把 成员函数当普通函数一样用,mem_fn 就是干这个的。
1 |
|
适用于回调场景中成员函数的统一封装。
3. std::not_fn(C++17)—— 对逻辑函数取反
如果你有个函数/谓词返回 bool,not_fn 可以生成一个“逻辑相反”的版本。
1 |
|
常用于
std::find_if_not、std::remove_if等 STL 算法的逻辑反转。
4. 仿函数(函数对象)+ std::function 一起用
C++ 把“重载 operator() 的类”也当作函数来使用。
1 | struct Add |
bind、lambda、仿函数等其实都能混合搭配出更高级的用法。
3. 小结
看到这里,你或多或少也该理解回调函数了吧,所以,还记得文章开头的伏笔吗?回调函数就是把函数当作参数传递使用,和传普通参数大同小异!简单回顾一下吧:
- 函数指针的定义:
返回类型 (*函数指针名)(参数列表);/返回类型 (函数名)(参数列表);。 - 取别名推荐使用:
using 别名 = 返回值类型 (*)(参数列表);,既因为typedef模板支持较弱,又因为语法糖 🍭。 - C++11 库支持后推荐使用:
std::function<返回类型(参数类型列表)> 变量名;。 bind和占位符:auto 新函数 = bind(函数地址, 参数1, 参数2, ...);;placeholders::_1,placeholders::_2,placeholders::_3, ……














