std::thread构造时默认按值拷贝所有参数;传引用必须显式用std::ref()或std::cref()包装,否则编译失败,且需确保被包装对象生命周期长于线程执行时间。

std::thread 构造时直接传参即可,但默认按值拷贝;要传引用必须显式用 std::ref() 或 std::cref() 包装,否则会编译失败或行为异常。
std::thread 传参默认是值传递
构造 std::thread 时,所有额外参数都会被完美转发(perfect forward)到目标函数,但底层实现会先对每个参数做一次拷贝——哪怕你传的是左值引用变量,也会被复制一份进新线程栈。这意味着:
- 大型对象(如
std::vector、std::string)可能触发不必要的深拷贝,影响性能 - 原变量的修改不会反映到线程内,线程里操作的是副本
- 若目标函数签名要求引用(如
void f(int&)),直接传变量会编译报错:error: no matching function for call to 'std::thread::thread(...)'
传引用必须用 std::ref() 包装
想让线程函数真正操作原始变量,就得用 std::ref() 把变量“包装”成可转发的引用包装器。它不是指针,也不是裸引用,而是一个轻量级代理对象,能跨线程边界安全传递引用语义。
常见错误写法:std::thread t(f, my_int); —— 即使 f 声明为 void f(int&),这行也通不过编译。
正确写法:
组合式C++代码评审方案,融合静态分析、AI推理、多轮迭代评审和C++专项检查,适用于PR审查、增量代码审查、全项目评审和代码质量评分,触发词包括review cpp、cpp代码评审、C++review、代码审查。
int counter = 0;
std::thread t([](int& x) { x++; }, std::ref(counter));
t.join();
// counter 现在是 1
-
std::cref()用于只读 const 引用场景,比如void f(const std::string& s) - 别对临时对象用
std::ref(),比如std::ref(std::string("hello")),会导致悬垂引用 -
std::ref()包装的变量生命周期必须长于线程执行时间,否则访问野引用
调用类成员函数时,第一个参数是对象指针或引用
非静态成员函数隐含一个 this 参数,所以 std::thread 构造时前两个实参分别是:成员函数指针、对象地址(或 std::ref(obj)),之后才是成员函数本身的参数。
例如:
struct Worker {
void work(int a, const std::string& s) { /* ... */ }
};
Worker w;
std::thread t(&Worker::work, &w, 42, std::string("hello")); // OK:传 this 指针
// 或者:
std::thread t2(&Worker::work, std::ref(w), 42, "world"); // OK:传 this 引用
- 不能传
w(值),因为成员函数需要绑定到具体对象实例,值传递会导致对象被拷贝,而线程里调用的是副本上的函数 - 若
Worker析构早于线程结束,且线程还在调用其成员函数,就会 crash —— 这是最容易被忽略的生命周期陷阱 - 静态成员函数没有
this,可直接像普通函数一样传参
线程对象析构前必须 join() 或 detach()
这是硬性约束:如果 std::thread 对象在未调用 join() 或 detach() 的情况下被销毁,程序会立即调用 std::terminate() 终止,没有任何异常捕获机会。
-
t.join()是最常用方式,主线程阻塞等待子线程结束,适合需同步结果的场景 -
t.detach()让线程后台运行,std::thread对象与之解绑,但此后无法控制或等待它——适用于纯异步日志、心跳等任务 - 务必在
join()前检查t.joinable(),避免对已join或已detach的对象重复调用 - 异常路径下容易漏掉
join(),建议用 RAII 封装(如scoped_thread)或在try/catch中补全
传参本身不难,真正的复杂点在于对象生命周期管理——尤其是引用和成员函数场景下,稍不注意就会出现悬垂指针、竞态或静默崩溃。别只盯着语法对不对,得时刻问自己:这个变量在线程跑的时候还活着吗?谁负责它的析构?
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!










