C++ 学习笔记(八):移动语义与右值引用
拷贝是把资源复制一份,移动是把资源的所有权直接转交出去。对于管理堆内存、文件句柄这类资源的对象,移动比拷贝快得多——拷贝要 malloc + memcpy,移动只是改几个指针。
一、左值和右值
理解移动语义之前先搞清楚左值和右值。
左值(lvalue):有名字、有持久地址,可以出现在赋值号左边:
1 | int x = 10; // x 是左值 |
右值(rvalue):临时的、没有名字,不能取地址,赋值后就消失:
1 | int y = x + 1; // x + 1 是右值,计算完就没了 |
区分的简单方法:能不能对它取地址。&x 合法,&42 不合法。
二、右值引用
C++11 新增了右值引用,用 && 表示,专门用来绑定右值:
1 | int & lref = x; // 左值引用,绑定左值 |
右值引用的意义在于:函数可以通过参数类型区分”调用方是要拷贝”还是”调用方不再需要这个对象了,可以把资源偷走”。
三、移动构造函数和移动赋值运算符
在 Rule of Three 的基础上,C++11 引入了 Rule of Five——管理资源的类通常需要定义五个特殊函数:
- 析构函数
- 拷贝构造函数
- 拷贝赋值运算符
- 移动构造函数
- 移动赋值运算符
1 | class Buffer { |
移动构造和移动赋值标注 noexcept 很重要——STL 容器(如 std::vector)在扩容时会判断移动构造是否 noexcept,只有 noexcept 才会用移动而不是拷贝,否则为了保证异常安全会退化回拷贝。
四、std::move
std::move 不移动任何东西,它只是把左值强制转换为右值引用,告诉编译器”这个对象我不用了,可以移动它”:
1 | Buffer a(256); |
1 | std::vector<Buffer> vec; |
std::move 之后原对象处于”有效但未指定”的状态——可以析构,可以重新赋值,但不能假设它还有什么有意义的内容。
五、返回值优化(RVO / NRVO)
函数返回局部对象时,编译器通常会做返回值优化,直接在调用方的地址上构造对象,完全跳过拷贝和移动:
1 | Buffer make_buffer(size_t size) { |
现代编译器(GCC、Clang)对 RVO 的支持非常好,C++17 更是在某些情况下强制要求 RVO。所以从函数返回对象不用担心性能,不需要返回指针或引用来”优化”。
六、完美转发
模板函数转发参数时,希望保留参数的左值/右值属性,用 std::forward:
1 | template<typename T> |
这里的 T && 不是右值引用,是万能引用(forwarding reference)——当 T 被推导时,T && 既可以绑定左值也可以绑定右值,配合 std::forward 把属性原样传递下去。
1 | int x = 10; |
完美转发主要用于实现通用工厂函数、emplace 系列接口等。
七、嵌入式里的实际意义
移动语义在嵌入式里的价值主要体现在两个地方:
避免不必要的深拷贝
如果你的类管理了大块 buffer,传递时用移动而不是拷贝,可以省掉 malloc + memcpy 的开销:
1 | // 把采集到的数据包移动进队列,不拷贝 |
unique_ptr 的所有权转移
unique_ptr 本身就依赖移动语义实现所有权转移,从函数返回 unique_ptr、放进容器,都是移动操作:
1 | auto sensor = create_sensor(0x48); // 移动构造 |
裸机项目里如果没用动态内存,移动语义用得很少。Linux 嵌入式或者用了 STL 容器的项目里,理解移动语义有助于写出性能更好的代码。
总结
- 左值有名字有地址,右值是临时的没有名字
- 右值引用
&&:绑定右值,配合重载区分拷贝和移动语义 - 移动构造/移动赋值:把资源”偷”过来,原对象置空,比深拷贝高效
noexcept:移动操作加上noexcept,STL 容器才会优先选择移动std::move:把左值转为右值引用,移动后原对象不能再用- RVO/NRVO:编译器自动优化函数返回值,不需要手动用指针”优化”
std::forward:完美转发,保留参数的左值/右值属性




