本节摘要:拷贝是"整屋家具再买一套",移动是"房产证过户、原主搬空"——右值引用
T&&标记"这个对象可被掏空",移动构造把堆指针偷过来而不逐字节复制。std::move本身不搬任何东西,它只是授权;完美转发则让模板把这份授权原样递给下一层。理解移动语义,容器扩容、函数返回、智能指针过户的性能账全部豁然开朗。
阅读完本节,你应当能够:
#include <iostream> #include <string> #include <utility> struct Widget { Widget(std::size_t n) : data_(new char[n]{}) {} ~Widget() { delete[] data_; } Widget(const Widget& o) : size_(o.size_), data_(new char[o.size_]) { std::copy(o.data_, o.data_ + size_, data_); std::cout << "拷贝构造:分配并复制 " << size_ << " 字节\n"; } Widget(Widget&& o) noexcept : size_(o.size_), data_(o.data_) { o.data_ = nullptr; o.size_ = 0; std::cout << "移动构造:只偷了两个指针值\n"; } std::size_t size_ = 0; char* data_ = nullptr; }; int main() { Widget a(1000000); Widget b = a; // 拷贝:百万字节逐个复制 Widget c = std::move(a); // 移动:两个整数赋值完事 } // 输出:拷贝构造:分配并复制 1000000 字节 // 移动构造:只偷了两个指针值
拷贝构造要 new 一块同尺寸堆内存、逐字节复制、保持来源完好;移动构造只把"指针与长度"两个标量接过户,再把来源置空防止双重释放。百万字节对八个字节,这就是移动语义的物理本质:不搬货物,只换货主。

左值有名字、能取地址(变量、数组元素、*p);右值是临时的、即将消亡的(字面量、函数按值返回的临时对象、std::move 的产物)。T&&(右值引用)绑定右值,等于给临时对象发了"器官捐献卡"——重载决议时,临时对象优先匹配 T&& 版本的构造/赋值,于是"反正要死,掏空它"的交易达成。
std::move 的名字有极大误导性:它一个字节也不搬,只是把表达式强制标记成右值(一次类型转换),真正干活的是随后被选中的移动构造函数。记住等价式:
Widget c = std::move(a); // 等价理解:请按移动方式从 a 构造 c
被移动后的对象处于"有效但未指明"状态——可以析构、可以赋新值,但别假设它还有原来的内容。std::string s2 = std::move(s1); 之后 s1 通常是空串,但这不保证,写依赖它非空的代码是错的。
三个高频疑问一次说清:
**函数按值返回要不要 move?**不用。return local; 触发返回值优化或隐式移动(C++11 起对局部变量自动按移动处理),写 return std::move(local); 反而禁用拷贝省略、画蛇添足。
**什么时候主动 move?**来源确实即将丢弃:把成员交给新主、把容器内容转交、把"要修改的按值参数"搬进容器(v.push_back(std::move(name));)。来源还要用就绝不能 move。
为什么移动构造要 noexcept?std::vector 扩容要把旧元素搬到新内存。若移动可能抛异常,搬到一半炸了,旧数据已残、新数据不全,无法恢复;于是标准库退回安全的逐个拷贝。不标 noexcept 的移动构造,在容器扩容时根本不会被使用——3.2 节 Buffer 已示范正确写法。
模板里把参数转发给下一层时,希望保持它原本的左值/右值身份——右值继续走移动,左值继续走拷贝。这就是万能引用(T&& 在模板推导语境)加 std::forward:
#include <memory> #include <utility> #include <string> template <typename T, typename... Args> std::unique_ptr<T> make(Args&&... args) { // 万能引用:左右通吃 return std::unique_ptr<T>( new T(std::forward<Args>(args)...)); // 原样转发:不丢身份 // 若直接写 new T(args...):右值退化成左值 全部走拷贝 性能白丢 } struct Big { explicit Big(std::string s) : s_(std::move(s)) {} std::string s_; }; auto p = make<Big>(std::string(1000000, 'x')); // 参数是右值 整条链路走移动
std::forward<Arg>(arg) 按模板推导记录的原身份还原类别。日常业务代码直接用 std::make_unique(内部正是这么写的),自己动手的机会主要在泛型工厂、emplace 系列封装等场合。记一条判断口诀:有名字的是左值——即便它的类型是右值引用,所以要显式 forward/move 才能恢复右值身份。
移动语义落地后,一串旧经验作废、一串新模式成立:
std::string,函数体内 move 进目标——调用方给右值就全程移动,给左值也只拷贝一次,代码比 const 引用加分支更简洁;vector<Widget> 扩容从"逐个深拷贝"变成"逐个过户",存大对象不再心疼;vector<unique_ptr<Shape>> 正是产物;💡 关键直觉:拷贝保护的是"来源的完整性",移动利用的是"来源的临终"。凡是你确知对象将死的地方(临时值、return、最后一次使用),都该把移动的路让出来——语言已铺好轨道,别让旧习惯把货车开回拷贝的旧路。
下一节从运行期回到编译期:模板如何按类型批量生成代码,让 RAII 与移动语义的成果自动覆盖所有类型。