定义
回调函数可以理解成:
把"之后要执行的逻辑"先交给别人,等某个时机到了,再由对方反过来调用你提供的函数。
也就是说,不是你主动直接调用它,而是你把它注册出去,等事件发生、任务完成、条件满足时,由别的代码来调用它。
一句话记忆:
回调 = 把函数当参数传出去,在将来的某个时刻被调用。
为什么需要回调
回调函数最核心的价值是解耦。
比如你写一个通用处理函数,它只负责"什么时候处理",但"处理完之后做什么"不想写死,就可以把后续逻辑交给回调。
常见用途有:
- 事件通知
- 异步任务完成后的处理
- 通用算法中的自定义策略
- GUI 按钮点击响应
- 网络请求返回后的处理
- 遍历容器时对每个元素执行某种操作
最基本的理解模型
你可以把它看成两部分:
-
提供者
也就是"我这里有个函数,之后你可以调它"
-
调用者
也就是"先把这个函数收下,等合适的时候我再调用"
例如:
你写了一个下载函数
下载函数接收一个"下载完成后执行什么"的函数
下载结束后,它去调用这个函数
这就是回调。
一、最传统的写法:函数指针回调
这是最基础、最接近 C 风格的回调方式。
示例:
cpp
#include <iostream>
void onResult(int value) {
std::cout << "result = " << value << std::endl;
}
void process(int x, void (*callback)(int)) {
int result = x * 2;
callback(result);
}
int main() {
process(10, onResult);
return 0;
}
运行逻辑:
- main 把 onResult 传给 process
- process 做完自己的工作
- process 再调用 callback(result)
- 实际上调用到 onResult
这就是最简单的回调模型。
函数指针回调的优点和缺点
优点:
- 简单
- 开销低
- 和 C 接口兼容
缺点:
- 只能传普通函数,不能直接保存有状态对象
- 不能很自然地绑定成员函数
- 表达能力比较弱
所以现代 C++ 里,纯函数指针通常只在和 C 接口交互、或者特别强调轻量时使用。
二、带参数的回调示例
比如写一个遍历函数,对每个元素执行用户传入的逻辑:
cpp
#include <iostream>
#include <vector>
void printItem(int x) {
std::cout << "item: " << x << std::endl;
}
void forEach(const std::vector<int>& data, void (*callback)(int)) {
for (int x : data) {
callback(x);
}
}
int main() {
std::vector<int> nums = {1, 2, 3, 4};
forEach(nums, printItem);
return 0;
}
这里 callback 就是"对每个元素怎么处理"的回调。
三、成员函数为什么不能直接当普通回调
很多人一开始最容易卡在这里。
看下面这个类:
cpp
#include <iostream>
class Handler {
public:
void onData(int x) {
std::cout << "data = " << x << std::endl;
}
};
成员函数和普通函数不一样,它调用时必须依赖对象。
也就是说:
- 普通函数地址就够了
- 成员函数除了函数地址,还需要一个对象
所以这种写法不行:
cpp
Handler h;
process(10, h.onData);
因为 h.onData 不是一个普通函数指针。
成员函数本质上需要:
- 成员函数指针
- 对应对象
四、现代 C++ 最常用:std::function
std::function 是现代 C++ 里最常见的回调包装器。
它可以统一接收很多种可调用对象:
- 普通函数
- Lambda
- 仿函数
- bind 之后的成员函数
- 其他可调用对象
示例:
cpp
#include <iostream>
#include <functional>
void process(int x, const std::function<void(int)>& callback) {
int result = x * 3;
callback(result);
}
void onResult(int value) {
std::cout << "result = " << value << std::endl;
}
int main() {
process(10, onResult);
return 0;
}
这个版本比函数指针灵活得多,因为 std::function<void(int)> 表示:
"任何能接收一个 int、返回 void 的可调用对象,我都能装。"
五、Lambda 作为回调
这是现代 C++ 里最推荐、最常见的方式。
示例:
cpp
#include <iostream>
#include <functional>
void process(int x, const std::function<void(int)>& callback) {
callback(x + 100);
}
int main() {
process(5, [](int value) {
std::cout << "callback value = " << value << std::endl;
});
return 0;
}
Lambda 的好处非常明显:
- 写法短
- 就近定义,代码更集中
- 可以捕获外部变量
- 比 bind 更直观
六、Lambda 捕获外部状态
回调经常不只是打印一下,而是要用到外部变量。
这时 Lambda 特别合适。
示例:
cpp
#include <iostream>
#include <functional>
void process(const std::function<void(int)>& callback) {
int result = 42;
callback(result);
}
int main() {
int total = 0;
process([&total](int value) {
total += value;
});
std::cout << "total = " << total << std::endl;
return 0;
}
这里回调捕获了 total,并在回调里修改它。
这个能力是普通函数指针做不到的。
七、成员函数回调:配合 Lambda
这是实际项目里最常见的成员函数回调写法。
cpp
#include <iostream>
#include <functional>
class Handler {
public:
void onResult(int value) {
std::cout << "Handler result = " << value << std::endl;
}
};
void process(int x, const std::function<void(int)>& callback) {
callback(x * 10);
}
int main() {
Handler h;
process(3, [&h](int value) {
h.onResult(value);
});
return 0;
}
这里本质上是:
- 把对象 h 捕获进 Lambda
- Lambda 内部再调用 h.onResult(value)
这通常比 std::bind 更清楚。
八、成员函数回调:std::bind 写法
如果你想学完整,也要知道 bind 的写法。
cpp
#include <iostream>
#include <functional>
class Handler {
public:
void onResult(int value) {
std::cout << "value = " << value << std::endl;
}
};
void process(int x, const std::function<void(int)>& callback) {
callback(x + 1);
}
int main() {
Handler h;
auto cb = std::bind(&Handler::onResult, &h, std::placeholders::_1);
process(10, cb);
return 0;
}
含义是:
- 绑定成员函数 Handler::onResult
- 绑定对象 h
- 保留一个参数位置给将来调用时传入
但现代 C++ 实战里,一般更推荐 Lambda,因为更容易读。
九、回调和仿函数
仿函数就是重载了 operator() 的对象。
它也可以作为回调。
cpp
#include <iostream>
#include <functional>
struct Printer {
void operator()(int value) const {
std::cout << "Printer: " << value << std::endl;
}
};
void process(int x, const std::function<void(int)>& callback) {
callback(x * x);
}
int main() {
process(6, Printer());
return 0;
}
仿函数适合:
- 有状态
- 可复用
- 逻辑比较完整
但如果只是临时回调,Lambda 往往更简单。
十、一个完整一点的回调场景:排序规则
标准库里有很多回调式设计,排序就是典型例子。
cpp
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> nums = {4, 1, 7, 2, 9};
std::sort(nums.begin(), nums.end(), [](int a, int b) {
return a > b;
});
for (int x : nums) {
std::cout << x << " ";
}
std::cout << std::endl;
return 0;
}
这里传给 sort 的比较函数,本质上就是回调。
排序算法在内部需要比较元素时,会反复调用你传入的 Lambda。
十一、异步里的回调
回调在异步编程里尤其常见。
例如:
- 发起网络请求
- 不能立刻拿到结果
- 请求完成之后,再调用你提供的回调函数
一个简化示意:
cpp
#include <iostream>
#include <functional>
void fakeAsyncRequest(const std::function<void(const std::string&)>& callback) {
std::string response = "success";
callback(response);
}
int main() {
fakeAsyncRequest([](const std::string& result) {
std::cout << "response = " << result << std::endl;
});
return 0;
}
这个例子没有真正开线程,但表达的结构是一样的:
你把"结果回来之后做什么"交给回调。
十二、C 风格回调中的上下文指针
如果你接触 C 库、操作系统 API、图形库、底层网络库,经常会见到这种设计:
- 一个函数指针
- 一个用户自定义上下文指针
因为纯函数指针没有状态,所以通常还要额外带一个 void* user_data。
示意代码:
cpp
#include <iostream>
typedef void (*Callback)(int, void*);
void process(int x, Callback cb, void* userData) {
int result = x * 5;
cb(result, userData);
}
struct Context {
int total;
};
void onResult(int value, void* userData) {
Context* ctx = static_cast<Context*>(userData);
ctx->total += value;
}
int main() {
Context ctx{0};
process(2, onResult, &ctx);
std::cout << "total = " << ctx.total << std::endl;
return 0;
}
这是很多 C 接口处理"回调需要状态"的经典方式。
十三、模板回调:性能更高的泛型写法
如果你只是想写一个通用接口,不一定非得用 std::function。
很多时候模板参数更高效,因为没有类型擦除成本。
示例:
cpp
#include <iostream>
template<typename Callback>
void process(int x, Callback callback) {
callback(x + 7);
}
int main() {
process(10, [](int value) {
std::cout << value << std::endl;
});
return 0;
}
这种写法的优点:
- 通常更容易被内联优化
- 没有 std::function 的额外包装成本
- 很适合泛型库代码
缺点:
- 接口类型不统一
- 不方便在类成员里长期保存不同类型的回调
所以简单理解:
- 临时调用、泛型高性能场景,用模板参数
- 需要统一存储、统一接口时,用 std::function
十四、什么时候用哪种回调方式
可以这样选:
-
普通函数指针
适合简单、轻量、C 兼容场景
-
std::function
适合需要统一接收多种可调用对象的接口
-
Lambda
适合大多数现代 C++ 回调场景,通常是首选
-
模板参数回调
适合高性能泛型代码
-
成员函数 + Lambda
适合对象方法回调,实际开发里最常见
十五、回调常见陷阱
1. 生命周期问题
这是最危险的点。
例如:
cpp
#include <iostream>
#include <functional>
std::function<void()> g_callback;
void setCallback(const std::function<void()>& cb) {
g_callback = cb;
}
int main() {
int x = 10;
setCallback([&x]() {
std::cout << x << std::endl;
});
g_callback();
return 0;
}
上面在 main 里立刻调用还没问题。
但如果这个回调被保存下来,在 x 已经失效后再调用,就会变成悬空引用。
所以异步回调里尤其要小心引用捕获。
经验是:
- 不确定生命周期时,优先按值捕获
- 如果捕获对象指针,要确保对象活得足够久
- 跨线程、延迟执行场景尤其要谨慎
2. this 指针悬空
很多人喜欢这样写:[this](int x) { handle(x); }
如果对象已经销毁,而回调以后才触发,就会出错。
更稳妥的做法通常是:
- 明确保证对象生命周期
- 或者配合智能指针,比如 weak_ptr 做安全检查
示意:
cpp
#include <iostream>
#include <memory>
#include <functional>
class Worker : public std::enable_shared_from_this<Worker> {
public:
std::function<void(int)> makeCallback() {
std::weak_ptr<Worker> weakSelf = shared_from_this();
return [weakSelf](int value) {
if (auto self = weakSelf.lock()) {
self->handle(value);
}
};
}
void handle(int value) {
std::cout << "handle " << value << std::endl;
}
};
这种模式在异步回调里很常见。
3. 回调过深导致代码难读
如果一层回调里再注册下一层回调,再下一层又回调,很容易写成"回调地狱"。
这在旧式异步代码里很常见。
解决思路通常是:
- 拆函数
- 让每个回调只做一件事
- 用对象封装状态
- 更复杂的异步场景可考虑 future、协程等机制
4. std::function 不是零成本
std::function 很方便,但它不是完全没有开销。
它有类型擦除成本,某些场景可能还有额外分配。
所以在极致性能场景里,模板回调通常更合适。
但一般业务代码里,不需要过度担心,优先写清楚。
十六、一个更接近实际项目的例子
做一个"按钮点击回调"的简单模拟:
cpp
#include <iostream>
#include <functional>
#include <string>
class Button {
public:
void setOnClick(std::function<void()> callback) {
onClick_ = std::move(callback);
}
void click() {
if (onClick_) {
onClick_();
}
}
private:
std::function<void()> onClick_;
};
int main() {
Button btn;
std::string name = "OK";
btn.setOnClick([name]() {
std::cout << "button " << name << " clicked" << std::endl;
});
btn.click();
return 0;
}
这个例子很典型:
- Button 不关心点击后具体干什么
- 外部把逻辑通过回调注册进去
- 点击时 Button 再调用它
这就是回调最标准的设计意义:模块解耦。
十七、回调和普通函数调用的区别
普通函数调用是:
我现在就主动调用它。
回调是:
我把函数交给别人,由别人决定何时调用。
区别不在"是不是函数",而在"控制权"。
回调的控制权在调用方手里,而不是在你手里。
十八、面试回答怎么说
如果面试里被问"什么是 C++ 回调函数",可以这样答:
C++ 回调函数本质上是把可调用对象作为参数传给另一个函数,等特定时机再由对方调用。它常用于事件处理、异步通知、策略定制和模块解耦。传统做法是函数指针,现代 C++ 更常用 Lambda、std::function,成员函数回调通常通过 Lambda 或 bind 绑定对象来实现。实际使用时最重要的是处理好对象生命周期和捕获变量的有效性。
如果面试官继续追问"现代 C++ 最推荐怎么写",可以答:
大多数场景优先用 Lambda;需要统一保存回调时用 std::function;性能敏感的泛型场景用模板参数;和 C 接口对接时用函数指针。
一句话总结
回调函数不是一种特殊函数类型,而是一种使用方式:
把将来要执行的逻辑交给别人,在合适的时机由对方反过来调用你。
============================== 分割线 ================================
1. 面试高频问答
-
什么是回调函数
回调不是一种特殊语法,而是一种使用方式。你把一个可调用对象交给别的模块,等特定时机到了,由对方反过来调用你。
-
回调和普通函数调用有什么区别
普通调用是你现在主动调它。
回调是你把"将来做什么"先注册出去,由别的代码决定何时调用。
-
C++ 里回调常见有哪些实现方式
函数指针、Lambda、仿函数、成员函数加对象、std::bind 生成的可调用对象、std::function 统一包装。
-
现代 C++ 为什么最常用 Lambda
因为它就近定义、可读性高、能捕获上下文、写成员函数回调也自然。大多数业务代码里,Lambda 是首选。
-
std::function 和模板回调怎么选
std::function 适合统一接口、长期保存、跨模块传递。
模板参数适合性能敏感场景,因为更容易内联,没有类型擦除开销。
一句话:要统一接口用 std::function,要极致性能用模板。
-
成员函数为什么不能直接当普通回调
成员函数调用需要对象,只有函数地址不够。
所以要么用 Lambda 把对象包进去,要么用 std::bind 绑定对象和参数。
-
回调最容易出什么问题
生命周期问题最大。
比如异步回调里按引用捕获局部变量,或者捕获 this,但对象先销毁了,后续一触发就会悬空。
-
std::bind 现在还常用吗
知道就行,实战里更多时候直接用 Lambda。
因为 Lambda 更直观,调试也更好。
-
如果面试官问"回调的价值是什么"
核心答案是解耦。调用方只负责"何时触发",业务方只负责"触发后做什么"。
-
30 秒面试回答模板
C++ 回调函数本质上是把可调用对象作为参数传给其他模块,等特定时机由对方调用。它常用于事件通知、异步任务、策略定制和模块解耦。传统方式是函数指针,现代 C++ 更常用 Lambda 和 std::function;成员函数回调通常通过 Lambda 或 bind 绑定对象。实际使用时最重要的是处理好捕获对象和变量的生命周期。
2. 回调、Lambda、bind、function 的关系图解
先记住一句最重要的话:
回调是一种机制,不是某个具体类型。
Lambda、普通函数、仿函数、bind 结果都是"可调用对象"。
std::function 是"统一包装器",用来装这些可调用对象。
关系图可以这样看:
cpp
回调机制
你要提供一段"以后再执行"的逻辑
↓
这段逻辑可以写成
普通函数
Lambda
仿函数
bind 生成的对象
↓
如果接口想统一接收它们
用 std::function<R(Args...)>
↓
框架 / 算法 / 线程 / GUI / 网络库 保存它
↓
将来某个时机触发调用
四者的角色分工:
-
Lambda
最常用的回调写法。能捕获状态,适合绝大多数现代 C++ 场景。
-
std::bind
适配器。它把已有函数和部分参数绑起来,生成一个新的可调用对象。能用,但今天通常更推荐 Lambda。
-
std::function
包装器。它不负责"生成逻辑",只负责"统一保存和调用逻辑"。
-
回调
是一种设计模式,表示"把逻辑注册出去,将来再触发"。
一段完整示意:
cpp
#include <functional>
#include <iostream>
void freeFunc(int x) {
std::cout << "freeFunc: " << x << std::endl;
}
struct Functor {
void operator()(int x) const {
std::cout << "Functor: " << x << std::endl;
}
};
struct Handler {
void onData(int x) {
std::cout << "Handler: " << x << std::endl;
}
};
void setCallback(const std::function<void(int)>& cb) {
cb(42);
}
int main() {
Handler h;
setCallback(freeFunc);
setCallback([](int x) {
std::cout << "Lambda: " << x << std::endl;
});
setCallback(Functor{});
setCallback(std::bind(&Handler::onData, &h, std::placeholders::_1));
setCallback([&h](int x) {
h.onData(x);
});
}
你可以把这段代码背成一句话:
- Lambda、普通函数、仿函数、bind 结果都能作为回调
- std::function 负责统一接住它们
- 最后由触发方在合适时机执行
实践上的推荐顺序:
- 默认先用 Lambda
- 需要统一存储时用 std::function
- 已有旧接口适配时才考虑 std::bind
- 和 C 接口交互时用函数指针加上下文指针
3. 项目里的 8 个实战示例
-
按钮点击事件
最典型的 GUI 回调模型。
class Button {
public:
void setOnClick(std::function<void()> cb) {
onClick_ = std::move(cb);
}
void click() {
if (onClick_) {
onClick_();
}
}
private:
std::function<void()> onClick_;
};
Button btn;
btn.setOnClick([] {
std::cout << "button clicked" << std::endl;
});
btn.click();
-
排序时传比较规则
标准库本身就大量使用回调思想。
std::vector<int> nums = {4, 1, 7, 2};
std::sort(nums.begin(), nums.end(), [](int a, int b) {
return a > b;
});
排序算法不关心你的业务规则,只在需要比较时回调你传入的比较器。
-
遍历容器时执行自定义逻辑
把"遍历"与"处理逻辑"拆开。
void forEach(const std::vector<int>& data,
const std::function<void(int)>& cb) {
for (int x : data) {
cb(x);
}
}
forEach(std::vector<int>{1, 2, 3}, [](int x) {
std::cout << x * 10 << std::endl;
});
-
异步任务完成通知
这是回调最常见的实际用途之一。
void asyncAdd(int a, int b, std::function<void(int)> cb) {
std::thread([=] {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
cb(a + b);
}).detach();
}
asyncAdd(3, 4, [](int result) {
std::cout << "result = " << result << std::endl;
});
这里最重要的点是:异步场景里尽量按值捕获,别随手引用捕获局部变量。
-
定时器超时回调
定时器、重试器、任务调度器都很常见。
void setTimeout(int ms, std::function<void()> cb) {
std::thread([ms, cb = std::move(cb)] {
std::this_thread::sleep_for(std::chrono::milliseconds(ms));
cb();
}).detach();
}
setTimeout(500, [] {
std::cout << "timeout" << std::endl;
});
-
成员函数作为回调
现代写法优先用 Lambda 包装成员函数。
class Worker {
public:
void onDone(int value) {
std::cout << "done: " << value << std::endl;
}
};
void runTask(const std::function<void(int)>& cb) {
cb(100);
}
Worker w;
runTask([&w](int value) {
w.onDone(value);
});
如果对象可能先销毁,就不能简单捕获引用或 this。
-
C 风格库接口回调
很多底层库都长这样:函数指针加用户数据。
using Callback = void()(int, void);
void c_api_run(Callback cb, void* userData) {
cb(42, userData);
}
struct Context {
int total = 0;
};
void onResult(int v, void* userData) {
auto* ctx = static_cast<Context*>(userData);
ctx->total += v;
}
Context ctx;
c_api_run(onResult, &ctx);
这种写法是为了弥补函数指针没有状态的问题。
-
高性能泛型回调
如果不需要统一存储,只想高性能调用,可以直接用模板。
template <typename Callback>
void process(int x, Callback cb) {
cb(x * 2);
}
process(21, [](int value) {
std::cout << value << std::endl;
});
这种方式通常比 std::function 更轻量,适合热路径、模板库、数值计算框架。
实战结论
记忆时直接用这四句话就够了:
- 回调是机制,不是类型。
- Lambda 是现代 C++ 里最常用的回调实现。
- std::function 用来统一保存和传递回调。
- 真正容易出问题的不是语法,而是生命周期和异步时机。
1. 面试题 20 题速刷版
-
什么是回调函数
回调不是特殊语法,而是一种使用方式:把一段可调用逻辑交给别的模块,在特定时机由对方反过来调用。
-
回调和普通函数调用的区别
普通调用是你主动调。回调是你先注册,调用时机由别的代码控制。
-
C++ 里常见的回调实现方式有哪些
普通函数、函数指针、Lambda、仿函数、成员函数配合对象、std::bind 结果、std::function。
-
现代 C++ 最常用哪种回调方式
大多数场景优先 Lambda;需要统一保存和传递时用 std::function。
-
std::function 是什么
它是可调用对象包装器,能统一保存"签名一致"的普通函数、Lambda、仿函数、bind 结果等。
-
std::function 的缺点是什么
它有类型擦除开销,某些情况下可能有额外分配,所以不是零成本抽象。
-
模板回调和 std::function 怎么选
要统一接口、便于保存时用 std::function;要性能和内联机会时用模板参数。
-
成员函数为什么不能直接当普通函数指针传
因为成员函数调用必须依赖对象,只有函数地址不够,还需要对象实例。
-
成员函数回调现代写法怎么做
通常用 Lambda 捕获对象,然后在 Lambda 里调用成员函数。
-
std::bind 现在还常用吗
知道原理即可,现代 C++ 里大部分场景 Lambda 更直观、更易维护。
-
回调最核心的价值是什么
解耦。框架只负责"什么时候触发",业务代码只负责"触发后做什么"。
-
回调最常见在哪些场景
事件处理、GUI、异步任务、网络请求完成通知、定时器、排序比较器、遍历容器时自定义操作。
-
C 接口里的回调为什么常带一个 void* user_data
因为纯函数指针没有状态,user_data 用来传上下文。
-
Lambda 为什么适合回调
因为写法近、可捕获上下文、可读性高,临时逻辑尤其方便。
-
Lambda 捕获最容易出什么问题
引用捕获悬空,尤其是异步执行、延迟执行、线程池回调场景。
-
为什么 [this] 容易出问题
因为它只捕获 this 指针,不延长对象生命周期;对象先销毁,回调再触发就会悬空。
-
weak_ptr 在回调里解决什么问题
它解决"对象可能提前销毁"的问题。回调触发时先 lock,成功再访问对象。
-
回调线程安全需要注意什么
共享状态要同步保护;不要默认认为回调在主线程执行;跨线程访问对象前要先确认生命周期和并发约束。
-
回调地狱是什么
一层回调里再套一层回调,控制流分散,代码难读难维护。解决思路是拆函数、封装状态,或者用 future、协程等模型。
-
面试里 30 秒总结怎么答
C++ 回调本质是把可调用对象交给别的模块,在特定时机由对方调用。传统方式是函数指针,现代 C++ 更常用 Lambda 和 std::function;成员函数回调通常通过 Lambda 绑定对象。真正的难点不在语法,而在生命周期、线程安全和异步执行时机。
2. Lambda 捕获、this 悬空、weak_ptr 安全回调详解
先记一句最重要的:
Lambda 能让回调更好写,但真正危险的是"捕获了谁"和"谁活得更久"。
Lambda 捕获的几种常见方式
-
值捕获
[x]把外部变量拷贝进 Lambda。通常更安全,适合异步回调。
-
引用捕获
[&x]Lambda 内部直接操作外部变量本体。同步短生命周期场景可以,用在异步里风险很高。
-
全值捕获
[=]按值捕获用到的外部变量。方便,但容易无意识拷贝太多状态。
-
全引用捕获
[&]按引用捕获用到的外部变量。写起来快,但最容易埋生命周期雷。
-
捕获 this
[this]捕获当前对象指针,不是捕获整个对象。对象销毁后,this 就悬空。
最常见错误 1:异步里引用捕获局部变量
cpp
#include <functional>
#include <iostream>
std::function<void()> g_cb;
void setCallback(std::function<void()> cb) {
g_cb = std::move(cb);
}
void bad() {
int x = 10;
setCallback([&x] {
std::cout << x << '\n';
});
} // x 在这里已经销毁
int main() {
bad();
// g_cb(); // 未定义行为:x 已经失效
}
问题本质:
回调被保存了,但 x 是局部变量,函数退出后已经没了。
更稳妥的写法是值捕获:
cpp
void good() {
int x = 10;
setCallback([x] {
std::cout << x << '\n';
});
}
这时 Lambda 里保存的是 x 的副本。
最常见错误 2:捕获 this 导致悬空
cpp
#include <functional>
#include <iostream>
std::function<void()> g_cb;
class Worker {
public:
void registerCallback() {
g_cb = [this] {
handle();
};
}
void handle() {
std::cout << "handle\n";
}
};
如果 Worker 对象已经销毁,再执行 g_cb(),就是典型悬空 this。
这里很多人误以为 [this] 是"把对象捕获进来了",其实不是。
它只是保存了一个裸指针。
什么时候 [this] 可以用
只有在你能明确保证这两点时才安全:
- 回调不会比对象活得更久
- 回调执行线程和对象销毁时机都可控
例如对象内部同步调用,问题不大:
cpp
class Worker {
public:
void runNow() {
auto cb = [this] {
handle();
};
cb();
}
void handle() {
std::cout << "ok\n";
}
};
这里 Lambda 没有逃逸出当前调用栈,通常安全。
异步回调里更推荐 weak_ptr
如果对象可能先销毁,而回调会延后触发,就用 weak_ptr。
cpp
#include <functional>
#include <iostream>
#include <memory>
class Worker : public std::enable_shared_from_this<Worker> {
public:
std::function<void()> makeSafeCallback() {
std::weak_ptr<Worker> weakSelf = shared_from_this();
return [weakSelf] {
if (auto self = weakSelf.lock()) {
self->handle();
}
};
}
void handle() {
std::cout << "safe handle\n";
}
};
int main() {
std::function<void()> cb;
{
auto worker = std::make_shared<Worker>();
cb = worker->makeSafeCallback();
cb(); // 输出 safe handle
}
cb(); // 对象已销毁,lock 失败,不执行 handle
}
这段代码的关键点:
- 不直接捕获
this - 捕获
weak_ptr - 执行前先
lock() - 只有对象还活着才继续调用成员函数
这就是异步场景里最常见的"安全回调模式"。
为什么不用 shared_ptr 直接捕获自己
也可以这样写:
cpp
auto self = shared_from_this();
return [self] {
self->handle();
};
这样能保证对象在回调执行前一直存活,但也有代价:
- 会延长对象生命周期
- 可能形成循环引用
- 某些事件系统里会导致对象迟迟不释放
所以经验上:
- 必须强保活时,用
shared_ptr - 只想"对象活着就执行,否则跳过"时,用
weak_ptr
引用捕获和值捕获怎么选
可以直接用这个判断规则:
- 同步、短作用域、确定不逃逸:引用捕获可以考虑
- 异步、延迟执行、线程池、事件系统:优先值捕获
- 捕获对象成员访问:优先考虑
weak_ptr或明确生命周期管理 - 不确定时,默认先避免
[&]和裸[this]
一个更接近项目的安全异步例子
cpp
#include <chrono>
#include <iostream>
#include <memory>
#include <thread>
class Session : public std::enable_shared_from_this<Session> {
public:
void start() {
std::weak_ptr<Session> weakSelf = shared_from_this();
std::thread([weakSelf] {
std::this_thread::sleep_for(std::chrono::milliseconds(200));
if (auto self = weakSelf.lock()) {
self->onComplete();
}
}).detach();
}
void onComplete() {
std::cout << "request complete\n";
}
};
这里线程 200ms 后才回调。
如果 Session 在这之前销毁,lock() 会失败,代码不会去访问失效对象。
回调里捕获容器、字符串等对象时的经验
- 小对象、只读数据,按值捕获通常更省心
- 大对象按值捕获可能有拷贝成本,要权衡
- 如果想转移所有权,可以用初始化捕获和
std::move
cpp
std::string msg = "hello";
auto cb = [text = std::move(msg)] {
std::cout << text << '\n';
};
这是现代 C++ 很实用的写法,尤其适合一次性异步任务。
最实用的结论
可以直接记下面这 6 条:
- 回调里最危险的不是 Lambda 本身,而是捕获对象和变量的生命周期。
[&]和[this]在异步场景里要高度警惕。- 不确定执行时机时,优先值捕获。
- 对象可能提前销毁时,不要直接捕获
this,优先weak_ptr。 - 需要强保活时才捕获
shared_ptr。 - 业务代码里 Lambda 是首选,安全性比"写得短"更重要。