c++中std::thread构造函数的注意事项
- 游戏开发
- 2025-09-09 11:51:02

目录
一、问题引出
二、示例代码及输出结果
三、详细解释
关键点解析
1. 第一次拷贝构造:临时对象(mData=101)
2. 第二次拷贝构造:线程内部存储对象(mData=102)
3. 第三次拷贝构造:线程函数参数 p4(mData=103)
析构顺序验证
结论
一、问题引出
函数原型详见 en.cppreference /w/cpp/thread/thread/thread
本文只讲第三个形式。
c++标准规定
根据C++的标准,当使用std::thread创建线程时,所有的参数都会被拷贝到线程的内部存储中,然后再传递给线程函数。这是因为线程可能在参数所在的作用域结束后才执行,所以必须确保参数的生存期足够长。所以,当传递对象作为参数时,会进行一次拷贝构造,创建该对象的副本,存储在线程的内部。
为了验证上述规则。写一段示例代码看看。
二、示例代码及输出结果代码:
#include<iostream> #include<thread> #include<string> class Foo{ public: Foo(int d):mData(d){ std::cout<<"Foo():"<<this<<" :"<<mData<<std::endl; } Foo(const Foo& foo){ mData = foo.mData + 1; std::cout<<"Foo(const Foo& foo):"<<this<<" :"<<mData<<std::endl; } ~Foo(){ std::cout<<"~Foo():"<<this<<" :"<<mData<<std::endl; } int getData() const{ return mData; } private: int mData; }; void threadFunc(int p1,float p2,std::string p3,Foo p4) { std::this_thread::sleep_for(std::chrono::seconds(2)); std::cout<<p1<<" ,"<<p2<<" ,"<<p3<<" ,"<<&p4<<":"<<p4.getData()<<std::endl; } int main(int argc,char* argv[]) { { std::thread t1; { float fd = 1.23; Foo foo1(100); //所有的参数都会被复制 t1 = std::thread(threadFunc,12,fd,"test string para",foo1); } t1.join(); } std::cout<<"exit"<<std::endl; return 0; }输出结果:
Foo():0x7fffd0445520 :100 Foo(const Foo& foo):0x7fffd04454a0 :101 Foo(const Foo& foo):0x55a08cdd72c8 :102 ~Foo():0x7fffd04454a0 :101 ~Foo():0x7fffd0445520 :100 Foo(const Foo& foo):0x7f5b04383d8c :103 12 ,1.23 ,test string para ,0x7f5b04383d8c:103 ~Foo():0x7f5b04383d8c :103 ~Foo():0x55a08cdd72c8 :102 exit先看内层块作用域的float fd变量。当内层块作用域结束之后,foo1和fd将失效,从第7行输出可以看到最终线程t1中仍然打印出了1.23,即正确的原来的fd的值。这就说明在构造t1是fd被拷贝到了t1线程内部。
再看foo1对象。原始foo1对象的析构是在内层块作用域结束时发生的,打印输出了第5行。从输出的第2行可以看到在构造线程t1时,确实是先发生了对foo1的拷贝构造。这印证了c++标准中的规定。
更多疑惑:为什么会发生三次拷贝构造?
第4行和第5行输出,表明在原始的foo1对象被析构之前先析构了第一次拷贝构造的对象(101)。这是为什么呢?如果第一次拷贝构造得到的对象(101)是线程内部存储的对象的话,那这个对象不应该这么早就被析构掉,而是应该跟随线程t1的生命周期在外层块作用域结束时被析构。所以有理由认为第一次拷贝构造得到的对象是一个临时对象,第二次拷贝构造得到的对象(102)才是线程内部存储的对象。第9行输出印证了这一观点。
以下是对这一段代码的详细解释。
三、详细解释 关键点解析通过代码和输出,可以明确三次拷贝构造的来源及析构顺序:
1. 第一次拷贝构造:临时对象(mData=101)
触发时机:当调用 std::thread 构造函数时,参数 foo1 需要被传递到线程的内部存储。
具体过程:
参数 foo1 是左值,需通过 decay-copy 生成一个临时副本。
此处触发第一次拷贝构造函数:
Foo(const Foo& foo):0x7fffd04454a0 :101析构时机:
这个临时对象在 std::thread 构造函数完成后立即析构(因为它仅用于初始化线程的内部存储)。
对应输出中的析构顺序:
~Foo():0x7fffd04454a0 :101 // 临时对象析构 ~Foo():0x7fffd0445520 :100 // 原始对象析构2. 第二次拷贝构造:线程内部存储对象(mData=102)
触发时机:线程启动时,需要将参数从主线程传递到新线程的上下文。
具体过程:
临时对象(mData=101)会被移动(或拷贝,若无移动语义)到线程的内部存储。
由于 Foo 未定义移动构造函数,此处触发第二次拷贝构造函数:
Foo(const Foo& foo):0x55a08cdd72c8 :102生命周期:
该对象存储在线程内部,直到线程执行完毕才会析构。
对应输出中的析构顺序:
~Foo():0x55a08cdd72c8 :102 // 线程内部对象析构(在 `t1.join()` 之后)3. 第三次拷贝构造:线程函数参数 p4(mData=103)
触发时机:线程函数 threadFunc 的参数 p4 是按值传递的。
具体过程:
线程内部存储的对象(mData=102)需要拷贝到 p4 中。
触发第三次拷贝构造函数:
Foo(const Foo& foo):0x7f5b04383d8c :103生命周期:
p4 在 threadFunc 执行结束时析构。
对应输出中的析构顺序:
~Foo():0x7f5b04383d8c :103 // 函数参数析构析构顺序验证
原始对象 foo1(mData=100):
在其所在块作用域结束时析构({ ... Foo foo1(100); ... } 结束)。
临时对象(mData=101):
在 std::thread 构造函数完成后立即析构。
线程内部存储对象(mData=102):
在 t1.join() 后析构(线程完全结束时)。
函数参数 p4(mData=103):
在 threadFunc 执行结束时析构。
输出结果与上述逻辑完全一致:
~Foo():0x7fffd04454a0 :101 // 临时对象析构 ~Foo():0x7fffd0445520 :100 // 原始对象析构 ~Foo():0x7f5b04383d8c :103 // 函数参数析构 ~Foo():0x55a08cdd72c8 :102 // 线程内部存储对象析构结论
第一次拷贝构造是为了生成临时对象,用于初始化线程内部存储。
第二次拷贝构造是将临时对象移动到线程的内部存储(由于缺乏移动语义,退化为拷贝)。
第三次拷贝构造是将线程内部存储的对象传递给函数参数 p4(按值传递)。
析构顺序由对象的生命周期决定:
临时对象和原始对象在主线程析构。
线程内部存储对象在线程结束后析构。
函数参数在函数结束时析构。
c++中std::thread构造函数的注意事项由讯客互联游戏开发栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“c++中std::thread构造函数的注意事项”
上一篇
微信小程序性能优化