【C++11 右值引用】

张开发
2026/4/20 8:10:10 15 分钟阅读

分享文章

【C++11 右值引用】
C11 右值引用总结从入门到移动语义一篇搞懂1. 为什么会有右值引用在 C98 里我们已经有了左值引用T它最大的作用就是减少拷贝提高传参和返回值效率。但是问题来了左值引用主要绑定左值很多临时对象、表达式结果、本来就只会“用一次”的对象其实没必要再深拷贝一份像string、vector这种管理资源的类拷贝成本很高所以 C11 引入了右值引用T和移动语义核心目标就一句话能偷资源就别傻乎乎深拷贝。2. 什么是左值什么是右值先记结论左值通常有名字、能取地址、生命周期相对稳定的对象右值通常是临时值、表达式结果、字面常量生命周期短往往不能稳定取地址来看例子inta10;// a 是左值int*pa;// *p 是左值std::string shello;s[0]H;// s[0] 是左值10;// 右值a1;// 右值std::string(hi);// 临时对象右值可以简单理解为类型特点例子左值能取地址、能持续存在a、*p、s[0]右值临时值、表达式结果10、ab、string(abc)3. 左值引用和右值引用3.1 左值引用inta10;intr1a;// 正确左值引用本质就是给左值起别名。3.2 右值引用intr210;// 正确doubler33.14;// 正确std::stringr4std::string(hello);// 正确右值引用本质也是给对象起别名只不过它绑定的是右值。4. 右值引用的几个核心规则4.1 左值引用不能直接绑定右值但const T可以intr110;// 错误constintr210;// 正确这也是为什么在 C98 中const T很常用因为它既能接左值也能接右值。4.2 右值引用不能直接绑定左值但可以绑定std::move(左值)inta10;// int r1 a; // 错误intr2std::move(a);// 正确4.3 右值引用变量本身是左值这个点特别重要也是最容易写错的地方。intrr10;// int rr2 rr; // 错误因为 rr 是有名字的变量表达式属于左值intrr2std::move(rr);// 正确一句话记忆右值引用类型的变量一旦有了名字使用它时就是左值。5. 右值引用可以延长临时对象生命周期std::strings1std::string(hello);conststd::strings2std::string(world);这两种写法都可以延长临时对象的生命周期。区别是const T延长生命周期但一般不能修改对象T延长生命周期而且可以修改对象例如std::stringsstd::string(abc);sdef;std::coutsstd::endl;// abcdef6. 函数重载中左值和右值是怎么匹配的voidf(intx){std::cout左值引用\n;}voidf(constintx){std::coutconst 左值引用\n;}voidf(intx){std::cout右值引用\n;}intmain(){inta1;constintb2;f(a);// 左值引用f(b);// const 左值引用f(10);// 右值引用f(std::move(a));// 右值引用}这也是右值引用在 STL 中能提升性能的基础左值走拷贝右值走移动。7. 右值引用真正的价值移动语义右值引用本身不是重点移动语义才是重点。移动语义的核心思想是拷贝重新申请资源再把旧资源复制一份移动直接把旧资源“搬过来”对于string、vector、list、map这种拥有堆资源的类移动往往比拷贝高效很多。7.1 移动构造classString{public:String(constchar*str){_strnewchar[strlen(str)1];strcpy(_str,str);}String(constStrings){_strnewchar[strlen(s._str)1];strcpy(_str,s._str);}String(Strings)noexcept:_str(s._str){s._strnullptr;}~String(){delete[]_str;}private:char*_strnullptr;};移动构造做的事非常直接接收一个右值引用参数把资源直接接管过来把源对象置为空避免析构时二次释放7.2 移动赋值Stringoperator(Strings)noexcept{if(this!s){delete[]_str;_strs._str;s._strnullptr;}return*this;}8.std::move到底是什么很多人在·会误以为std::move会“移动对象”。其实不是。std::move本质上只是一个强制类型转换它只是把一个对象转换成“可以被当成右值处理”的形式。你可以粗略理解成templateclassTtypenamestd::remove_referenceT::typemove(Tt){returnstatic_casttypenamestd::remove_referenceT::type(t);}所以一定要记住std::move本身不移动资源真正移动资源的是移动构造、移动赋值这些函数。9. 右值引用如何解决传值返回效率问题先看一个普通返回值std::stringfunc(){std::string shello world;returns;}这个写法在 C11 里通常很高效原因有两个编译器可能做RVO / NRVO返回值优化即使没优化只要类支持移动构造也可以走移动而不是拷贝所以现代 C 里按值返回对象通常不是问题。但是千万不要这样写std::stringfunc(){std::string shello;returnstd::move(s);// 错误}原因很简单s是局部对象函数结束后它就析构了你返回的是一个悬空引用一句话记忆不要返回局部对象的右值引用。10. 右值引用在传参中的提效STL 容器在 C11 后大量增加了右值引用版本接口例如push_back(constTx);// 拷贝push_back(Tx);// 移动这样就能区分传左值拷贝进去传右值移动进去例如std::vectorstd::stringv;std::string shello;v.push_back(s);// 拷贝v.push_back(std::move(s));// 移动v.push_back(world);// 临时对象通常也走移动/直接构造11. 值类别补充prvalue、xvalue、glvaluelvalue左值prvalue纯右值比如10、abxvalue将亡值比如std::move(a)glvalue泛左值包含左值和将亡值std::move(x)产生的是一个将亡值xvalue表示“这个对象的资源可以被搬走了”。12. 引用折叠模板里必须会的点C 里不能直接写“引用的引用”但在模板推导时会出现这种效果于是就有了引用折叠规则T 折叠成TT 折叠成TT 折叠成TT 折叠成T一句话记忆只有“右值引用 右值引用”还是右值引用其他情况全都折叠成左值引用。13. 完美转发为什么要std::forward看下面这个模板templateclassTvoidwrapper(Tx){func(x);// 注意x 有名字所以这里它是左值}虽然x的类型可能是右值引用但只要它有名字传下去时就是左值。这时如果我们希望“传进来是什么属性传下去还是什么属性”就要用完美转发templateclassTvoidwrapper(Tx){func(std::forwardT(x));}std::forward和std::move的区别std::move无脑转成右值std::forward有条件地保持原始值类别一句话move是强转右值forward是按原属性转发。14. 编译器默认生成移动构造/移动赋值的规则这部分很容易和拷贝构造混在一起复习时记这几条就够了如果类没有自己写移动构造、移动赋值、拷贝构造、拷贝赋值、析构函数编译器通常会尝试生成默认移动操作如果你自己写了拷贝构造、拷贝赋值或析构函数编译器往往就不会再自动帮你生成移动操作如果你自己声明了移动构造或移动赋值拷贝操作也通常不会再按你“以为的方式”自动生成所以实际开发里常见建议是一个类如果自己不直接管理资源就尽量一个特殊成员函数都别手写如果它自己管理资源那拷贝/移动/析构这一整套就最好一起认真处理。15. 补充常考点15.1 被move之后的对象还能不能用能用但状态通常是“有效但未指定”。例如std::string shello;std::string s2std::move(s);这时s还活着但你不要假设它一定是空串。你可以给它重新赋值、析构、清空但不要依赖它原来的值。15.2const对象基本没法真正移动conststd::string shello;std::string s2std::move(s);// 大概率还是拷贝不是移动因为移动通常需要修改源对象资源指针而const不允许改。15.3 为什么很多移动构造都建议加noexcept因为标准库容器在扩容时为了保证异常安全常常会优先选择“不抛异常的移动构造”。如果你的移动构造可能抛异常某些场景下容器宁可拷贝也不愿意移动。16. 一组最容易写错的代码错误 1把有名字的右值引用继续当右值用intrr10;// int rr2 rr; // 错误intrr2std::move(rr);// 正确错误 2返回局部对象的右值引用std::stringf(){std::string sabc;returnstd::move(s);// 错误}错误 3在模板里忘记完美转发templateclassTvoidwrapper(Tx){func(x);// x 永远是左值表达式}正确写法templateclassTvoidwrapper(Tx){func(std::forwardT(x));}

更多文章