想象你搬新家。旧家里有一整柜书,要把它们弄到新家去。笨办法是把每本书复印一遍送进去——慢、费纸、还占地方;聪明的办法是请搬家公司把整个书柜搬上卡车,原封不动运到新家,旧家只剩一个空书柜。前者叫“深拷贝”,后者叫“移动”。C++11 之前,C++ 处理大对象(vector、string、张量)时几乎只会“复印”,性能白白浪费。C++11 引入的移动语义,相当于给 C++ 请来了一支专业搬家公司:右值引用是给“临时快递”开的快速通道,std::move 是在箱子上贴“此箱可搬走”的标签,移动构造和移动赋值是搬家师傅手里的“转移确认单”。读完这篇,你会搞清楚左值和右值怎么区分、移动到底省了什么开销、编译器什么时候免费帮你搬、完美转发又是怎么一回事。

岗位关联

对 AI 从业者而言,移动语义是高性能 C++ 算子、推理引擎与训练框架优化的基本功。理解了它,你才能看懂底层张量为什么传得快,也才能写出真正省内存、低延迟的代码。

目录
  1. 可视化演示
  2. 左值与右值
  3. 右值引用
  4. std::move语义
  5. 移动构造
  6. 移动赋值
  7. 编译器优化
  8. 完美转发
  9. 性能实践
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

对比拷贝语义与移动语义的资源流转过程,展示右值引用如何实现零拷贝资源转移,大幅提升性能。

本节配有交互动效(进入视口后自动加载,约 3KB)。

拷贝vs移动语义 拷贝:深拷贝分配新内存,逐字节复制数据,源对象保持不变 移动:指针所有权转移,源对象置空,零内存分配 T(T&& other) : ptr(other.ptr) { other.ptr = nullptr; } 移动语义避免不必要的深拷贝开销

左值与右值

想象你家里有两类物品。一类是“长住户”——沙发、书架,它们有名字、有固定位置,你随时去看都在那里,可以反复使用;另一类是“临时快递”——外卖、驿站送来的包裹,没有名字,放在门口,取出来用完就扔。C++ 里的每个表达式,也都有这两种身份之一:左值是“长住户”,右值是“临时快递”。

左值(lvalue)是有名字、有持久地址的表达式结果,比如变量 a、数组元素 v[0];右值(rvalue)是临时生成的中间值,比如字面量 42、表达式 a + b 的结果,这条语句一执行完它就销毁。判断身份最朴素的办法是“取地址测试”:对 x 写 &x,编译器接受了,x 就是左值;报错了,x 就是右值。

这个区别之所以重要,是因为“临时快递”反正马上要销毁,不如把它的资源“抢”过来再利用——这正是移动语义的前提。C++11 把右值进一步细分,其中“将亡值”(xvalue)专指那些已经走到生命尽头、等着被“拆包回收”的对象。理解了左和右,才能理解后面所有机制为什么存在。

lvalue:有名字可取址,表达式结束后仍存在rvalue:临时值,语句结束即销毁\text{lvalue:有名字可取址,表达式结束后仍存在} \qquad \text{rvalue:临时值,语句结束即销毁}

1. 左值定义:有名字、能被取地址的对象,语句结束后它依然存活,可反复读写。

lvalue: &x 合法,对象跨语句存活\text{lvalue: } \&x \ \text{合法,对象跨语句存活}

2. 右值定义:临时产生的值,没有名字,不能取地址,语句结束立即销毁。

rvalue: &x 非法,语句结束即销毁\text{rvalue: } \&x \ \text{非法,语句结束即销毁}

3. 取址判断法:能不能对表达式取地址,是区分左右值的最快直觉。

&x 合法⇒左值&x 非法⇒右值\&x \ \text{合法} \Rightarrow \text{左值} \qquad \&x \ \text{非法} \Rightarrow \text{右值}

搬家时,沙发要继续留着用(左值),拆完的快递盒直接交给回收(右值)——后来的所有搬运策略,全都取决于物品的“身份”。

右值引用

家里贴标签有讲究:左值引用 T& 像一张“预约卡”,只能挂在确定存在的家具上;临时快递没有名字,预约卡挂不上去。右值引用 T&& 是专门开的一条“快速通道”:快递一到,通道直接接住它,当场拆包搬运。

右值引用的语法是“类型名后加两个 &”,专门绑定右值。有了它,函数就能“截胡”临时对象的资源,而不是眼睁睁看着它在语句结束时被白白销毁。它是移动语义的语法地基。

声明 std::string&& r = "temporary"s; 可以绑定临时字符串,但这只是入门用法。右值引用的真正战场是函数重载与参数传递:void f(const string&) 表示“我只借来读一读”,void f(string&&) 表示“我要接管你的资源”。注意区分:在具体类型上的 T&& 是右值引用;在模板推导里的 T&& 是“万能引用”,行为完全不同,第七节会细讲。右值引用本身并不快,快的是“接管资源”这个动作。

T&&  r=临时对象T&  l=有名变量T\&\& \; r = \text{临时对象} \qquad T\& \; l = \text{有名变量}

1. 右值引用绑定右值:只有临时对象这类右值,才允许绑定到 T&& 上。

T&&  r=临时对象;合法T\&\& \; r = \text{临时对象}; \quad \text{合法}

2. 左值引用绑定左值:有名字的变量用 T& 绑定,语义是“长期借用”。

T&  l=有名变量;合法T\& \; l = \text{有名变量}; \quad \text{合法}

3. 绑定规则互不串门:左值引用绑不上右值,右值引用绑不上左值(除非用 std::move 转换身份)。

右值↛T&左值↛T&&\text{右值} \not\to T\& \qquad \text{左值} \not\to T\&\&

有了“快速通道”,搬家公司才能专车接临时件,同时不占用给长期住户的预约卡——两条通道各司其职,互不干扰。

std::move语义

搬家师傅只认“可搬运”标签。你家旧家具明明还能用(是左值),可你也想让它被搬走,怎么办?贴一张标签:std::move 就是这张“此箱可搬走”的标签。它没有搬任何东西,只是告诉编译器:请把这家伙当成临时快递来对待。

std::move(x) 的本质是一个编译期转换:static_cast<T&&>(x)。它把左值 x 伪装成右值,从而让移动构造函数、移动赋值运算符愿意接手。整个过程零运行时开销,机器码里不会多出一条指令。

move 之后,源对象 x 的资源大概率被“搬家公司”接走了,x 进入“合法但未指定”(valid but unspecified)的状态——可以被销毁、可以被重新赋值,但不要读它的值。典型场景:vec.push_back(std::move(big)) 把大容器塞进 vector,queue.push(std::move(data)) 把大字符串送入队列。注意:对 int、double 这类基础类型,move 没有任何收益,因为拷贝本身就是逐字节复制。

std::move(x)≡static_cast<T&&>(x)\text{std::move}(x) \equiv \text{static\_cast}<T\&\&>(x)

1. 类型转换而非移动:std::move 只把 x 的类型从左值引用转为右值引用,不碰任何数据。

std::move(x)=static_cast<T&&>(x)\text{std::move}(x) = \text{static\_cast}<T\&\&>(x)

2. 零运行时开销:所有工作发生在编译期,生成的机器码里没有任何额外指令。

运行时开销=0\text{运行时开销} = 0

3. 配合移动构造函数:转换后的右值身份,才能让重载决议选中 A(A&&)。

move(x)⇒匹配 A(A&&) 移动版本\text{move}(x) \Rightarrow \text{匹配 } A(A\&\&) \ \text{移动版本}

标签本身不搬家,但它决定了师傅愿不愿意接单。贴对标签,大对象就能搭上移动的顺风车。

移动构造

搬家师傅手里有一张“转移确认单”。师傅把整座书柜搬上书车,在确认单上签:书柜所有权归新家,旧家书柜位留空。移动构造函数,就是这张确认单的代码版。

移动构造函数的签名是 A(A&& other)。它接收一个右值引用,把 other 里面的堆指针“顺手牵羊”拿过来,再把 other 的指针置空。如果不置空,两个对象会指向同一块内存,析构时就会“双重释放”(double free)导致崩溃。

对比开销:拷贝构造要申请新内存并逐元素复制,是 O(n) 的深拷贝;移动构造只是两次指针赋值,是 O(1) 的资源交接。对一个大 vector 来说,移动构造只交换三四个指针字段,快几个数量级。还有一个细节千万别漏:标上 noexcept。vector 扩容时,如果移动构造函数可能抛异常,标准库为了强异常安全保证,只敢退回去用拷贝;标了 noexcept,扩容就从 O(n) 拷贝秒变 O(1) 移动。

A(A&&o) noexcept:ptr(o.ptr)  {  o.ptr=nullptr;  }A(A\&\& o)\, \text{noexcept} : ptr(o.ptr) \; \{ \; o.ptr = \text{nullptr}; \; \}

1. 接管资源:直接用 other 的指针初始化自己的指针,不申请新内存。

ptr(o.ptr) ⇒ 两对象暂指同一资源ptr(o.ptr) \ \Rightarrow \ \text{两对象暂指同一资源}

2. 置空源对象:把 other 的指针设为 nullptr,防止析构时双重释放。

o.ptr=nullptr ⇒ 源变为空壳o.ptr = \text{nullptr} \ \Rightarrow \ \text{源变为空壳}

3. noexcept 标注:承诺移动过程不抛异常,让容器扩容时放心用移动。

noexcept ⇒ vector 扩容用移动而非拷贝\text{noexcept} \ \Rightarrow \ \text{vector 扩容用移动而非拷贝}

确认单一签,旧家空书柜就是源对象的“置空指针”。双方各得其所,谁也不会因为同一块内存被释放两次而崩溃。

移动赋值

移动构造是把家具搬进“空房子”;移动赋值是把家具搬进“已经住人的房子”——你得先把新家原来的旧家具扔出去(释放已有资源),再把新家具放进来。

移动赋值运算符是 A& operator=(A&& other),它要干四件事:判断是否自赋值、释放当前持有的旧资源、接管 other 的资源、把 other 置空。顺序千万不能乱。

自赋值检查 if (this != &other) 不能省。试想 a = std::move(a):如果先释放旧资源,而 other 恰恰就是 a 自己,那资源被释放后指针就悬空了,再“接管”只会得到一个野指针。检查通过后,先 delete ptr 释放旧内存防泄漏,再接管 new 指针,最后把源指针置空,返回 *this 支持链式赋值。

A&operator=(A&&o) noexcept  {  if(this≠&o)  {  释放旧资源;接管;置空源  }  return∗this;  }A\& \text{operator}=(A\&\& o)\, \text{noexcept} \; \{ \; \text{if}(this \ne \&o)\; \{\; \text{释放旧资源;接管;置空源}\; \} \; \text{return} \ast this; \; \}

1. 自赋值检查:this != &other 防止自己搬自己时资源先被释放。

if(this≠&o) ⇒ 规避自毁\text{if}(this \ne \&o) \ \Rightarrow \ \text{规避自毁}

2. 释放已有资源:先回收当前对象手里的旧内存,避免内存泄漏。

delete 旧资源 ⇒ 腾出空间\text{delete 旧资源} \ \Rightarrow \ \text{腾出空间}

3. 接管并置空源:把源指针拿过来,再把源指针置空。

ptr=o.ptr;o.ptr=nullptrptr = o.ptr; \quad o.ptr = \text{nullptr}

4. 返回 *this:返回对象自身的引用,支持链式赋值。

return∗this ⇒ a=b=c\text{return} \ast this \ \Rightarrow \ a = b = c

搬进住人的新房:清空、搬入、留空旧屋——三步闭环,内存安全又高效。

编译器优化

搬家公司有条“免费顺风车”服务。你打电话说“先搬到中转站,再转发到新家”,师傅一看出路直达,直接开进新家,连中转站都不用去。编译器对返回临时对象的处理就是这样:直接在函数返回的位置把对象构造好,拷贝和移动统统省略。

RVO(返回值优化)针对 return T(...) 这种返回临时值的情况;NRVO(具名返回值优化)针对 return 具名局部变量 的情况。两者都是编译器允许的“复制省略”(copy elision),C++17 起部分场景已被标准化为强制行为。

很多人误以为 return std::move(x) 更快,这其实是画蛇添足:一旦把 x 转成右值,编译器就不能再省略,必须真的调用一次移动构造函数。正确的做法是原样写 return x;,让编译器尽情发挥省略优化。写 return std::move(x) 等于放着免费顺风车不坐,非要中途下车再打车。

T  f()  {  return  T(…);  }⇒直接构造到调用处,零拷贝、零移动T\; f() \; \{ \; \text{return} \; T(\ldots); \; \} \quad \Rightarrow \quad \text{直接构造到调用处,零拷贝、零移动}

1. RVO 优化临时返回值:return 一个临时构造的对象时,直接在调用处内存上建好。

return T(…) ⇒ 调用处直接构造\text{return } T(\ldots) \ \Rightarrow \ \text{调用处直接构造}

2. NRVO 优化具名局部变量:return 一个具名局部变量,编译器同样可以省略搬运。

return x ⇒ 可省略拷贝/移动\text{return } x \ \Rightarrow \ \text{可省略拷贝/移动}

3. return std::move 的坑:转成右值后强制调用移动构造,复制省略机会被阻断。

return std::move(x) ⇒ 禁止省略,多付一次移动\text{return } \text{std::move}(x) \ \Rightarrow \ \text{禁止省略,多付一次移动}

免费的顺风车,你偏要中途下车再打车,费钱又费时。写返回代码时,让编译器做它最擅长的事。

完美转发

物流中转站的工作准则:包裹从发货人发到收货人,中转站不拆包、不复称,原封转发,还要把原始标记(普通件/加急件、易碎品)一并保留。完美转发就是这样一台“保真中转站”。

模板函数里写 template<class T> void wrapper(T&& x),这里的 T&& 不是右值引用,而是“万能引用”(forwarding reference):实参是左值时,它推导成左值引用;实参是右值时,它推导成右值引用。要在函数内部把参数原样转给另一个函数,就用 std::forward<T>(x)。

支撑这一切的是引用折叠规则:当 T 被推导为引用类型时,多个 & 会折叠——T& &、T&& &、T& && 都折叠成 T&,只有 T&& && 折叠成 T&&。加上可变参数模板 Args&&...,可以把任意数量、任意类别的参数“原封不动”转发给目标函数。现代 C++ 里 std::make_shared、容器的 emplace_back、各种“完美转发工厂”都建立在这块地基上。

T&&&→T&T&&&&→T&&std::forward<T>(x) 按原类别保真转发T\& \&\& \to T\& \qquad T\&\& \&\& \to T\&\& \qquad \text{std::forward}<T>(x) \ \text{按原类别保真转发}

1. 引用折叠规则:模板推导时多个引用符号按规则折叠,决定最终参数类型。

T&&→T&,T&&&→T&,T&&&→T&,T&&&&→T&&T\& \& \to T\&, \quad T\&\& \& \to T\&, \quad T\& \&\& \to T\&, \quad T\&\& \&\& \to T\&\&

2. 万能引用推导:形如 T&& 且 T 是模板参数时,左值实参推出 T = X&,右值实参推出 T = X。

f(左值)⇒T=X&f(右值)⇒T=Xf(\text{左值}) \Rightarrow T = X\& \qquad f(\text{右值}) \Rightarrow T = X

3. forward 保真转发:std::forward<T>(x) 只恢复 x 原本的左值或右值身份。

std::forward<T>(x)=static_cast<T&&>(x)\text{std::forward}<T>(x) = \text{static\_cast}<T\&\&>(x)

中转站不乱拆包:加急件到收货人手上还是加急件,临时件还是临时件。接收方拿到原样的包裹,才能做出正确的搬运决策。

性能实践

请到好搬家公司之后,还得会用三招:家电打包让师傅顺路带(依赖 RVO)、家具交接在确认单上盖章(用 std::move)、搬完别惦记旧家(别读已移动的源对象)。三招到位,搬家又快又省。

移动语义对大数据结构是 O(1) 的指针交接,深拷贝是 O(n) 的逐元素复制。机器学习底层处处受益:Tensor 在算子间的传递、大字符串的拼接、容器的扩容,省掉的都是一块块 memcpy。

实战要点归纳:第一,返回大对象直接 return 局部变量,交给 RVO/NRVO 处理,别加 move;第二,向容器插入不再使用的大对象时用 std::move(obj),把深拷贝变成指针交接;第三,移动之后不要读取源对象,它处于“合法但未指定”状态;第四,自定义类记得把移动构造和移动赋值标为 noexcept,否则 vector 扩容时宁可用慢速拷贝也不用移动。用 perf 等工具对比,能直观看到大块 memcpy 从火焰图中消失。

移动交接:O(1) 指针级深拷贝:O(n) 逐元素复制\text{移动交接:} O(1) \ \text{指针级} \qquad \text{深拷贝:} O(n) \ \text{逐元素复制}

1. 移动交接复杂度:资源指针直接过户,与元素个数无关,恒定时间完成。

移动耗时=O(1)\text{移动耗时} = O(1)

2. 深拷贝复杂度:逐元素复制到新内存,耗时与对象规模线性相关。

拷贝耗时=O(n)\text{拷贝耗时} = O(n)

3. 性能来源:省掉的是大量内存搬运指令与缓存未命中,而非魔法。

省下的≈memcpy(n) 与两次指针赋值之差\text{省下的} \approx \text{memcpy}(n) \ \text{与两次指针赋值之差}

搬家公司请对了、单子签对了、标签贴对了,剩下的就是享受省下来的时间和内存。高性能 C++ 的日常,就在这些细节里。

AI 应用场景

深度学习推理引擎中的张量传递

阶段:模型部署 / 推理优化

解决的问题:PyTorch、TensorRT 的自定义 C++ 算子中,临时张量在算子链里层层传递。若按值传递或触发深拷贝,显存带宽被 memcpy 占满,推理延迟居高不下,尤其是大 batch 的 Transformer 模型。

具体案例:自定义 LayerNorm 算子的中间 Tensor 不再拷贝,用移动构造交接到下一算子;输出张量直接 return 局部对象,依赖 RVO 零拷贝写出。改动后单算子延迟降低 30% 以上。

训练数据预处理流水线

阶段:数据加载 / 预处理

解决的问题:读取器解析出的超大 string、float 数组要从解码线程塞进 Batch 队列,传统拷贝让每个 batch 多花几十毫秒,GPU 喂不饱。

具体案例:queue.push(std::move(sample)) 把深拷贝变成指针交接;vector<Sample> 预分配后用移动赋值填充,避免扩容时的批量拷贝。训练吞吐显著提升。

高频服务端日志与序列化

阶段:在线服务 / 日志采集

解决的问题:大日志缓冲区和序列化结果在多个子系统间传递,反复拷贝拖垮吞吐,服务端 CPU 大量烧在内存复制上。

具体案例:日志库把 std::string 缓冲区通过移动语义交接给异步写盘线程,源缓冲置空后复用,单机 QPS 明显提升,GC 与内存碎片压力同步下降。

Python 演示

python45 行
import copyimport time  class Buffer:    """模拟 C++ 中拥有堆资源的大对象"""     def __init__(self, size=0):        self.data = list(range(size))     def copy_into(self):        """深拷贝:逐元素复制数据,耗时 O(n)"""        b = Buffer()        b.data = copy.deepcopy(self.data)        return b     def move_out(self):        """模拟移动:交接所有权,耗时 O(1),源对象被置空"""        b = Buffer()        b.data = self.data        self.data = []        return b  def bench(times=500):    copy_t = 0.0    move_t = 0.0    for _ in range(times):        src = Buffer(20000)        t0 = time.perf_counter()        dst = src.copy_into()        copy_t += time.perf_counter() - t0         src = Buffer(20000)        t0 = time.perf_counter()        dst = src.move_out()        move_t += time.perf_counter() - t0     print(f"深拷贝总耗时: {copy_t:.4f}s")    print(f"移动(所有权交接)总耗时: {move_t:.4f}s")    print(f"移动比拷贝快约: {copy_t / move_t:.1f} 倍")  if __name__ == "__main__":    bench()
目的:用纯 Python 模拟 C++ 移动语义的核心差异——深拷贝逐元素复制,移动只交接所有权引用,并量化两方案的时间差距。
  1. b.data = copy.deepcopy(self.data)对应 C++ 深拷贝的 O(n) 逐元素复制,最耗时的操作。
  2. b.data = self.data直接接管源列表引用,对应 C++ 移动构造的“偷指针”,O(1)。
  3. self.data = []把源对象置空,对应移动后把源指针置为 nullptr,防止两个对象共享同一资源。
  4. copy_t / move_t量化两种方案的开销差距,让你直观感受“整柜搬走”比“逐本复印”快多少。

常见误区

误区我原本以为 move 一执行,数据就被移走了,源对象立刻就变空了,所以单写一行 `std::move(a)` 应该有点作用。

正解std::move 只是把一个左值转换成右值引用的编译期转换,什么数据都没碰。真正“搬走”发生在后续调用移动构造或移动赋值时。所以单独的 std::move(a); 毫无效果;传给 const& 参数的函数时,move 也起不了任何搬运作用,因为它选中的仍是拷贝路径。

为何常见:函数名叫"move",动词感太强,让人天然以为有个搬运动作;而调试器里看不到任何变化,更让初学者困惑。

误区移动完成之后,源对象肯定被清空了,我可以放心读取它的内容来复用。

正解标准只保证源对象处于“合法但未指定”状态:可以安全析构、可以被重新赋值,但它的具体值不确定。常见实现会把指针置空,但这是实现细节,不是语言保证。搬家后旧房可能还留着垃圾,你绝不能数里面有几本书。

为何常见:教学示例为了展示资源转移,总是展示“置空”的典型实现,读者容易把实现细节当成语言承诺。

误区我想返回一个大对象,先用 std::move 包一下,反正移动比拷贝快,double 保险。

正解这恰好帮倒忙。写 return x; 时编译器可以做 NRVO,连移动都省了;写成 return std::move(x);,x 变成右值,反而强制调用移动构造函数,白白丢掉一次零成本的省略机会。正确姿势是原样返回局部变量,把优化空间留给编译器。

为何常见:“移动比拷贝快”的观念深入人心,却没意识到编译器免费的省略(零成本)比移动(低成本的指针交接)更优。

误区给参数加上 `&&` 就能快,所有类都应该写移动构造函数,反正写了肯定有好处。

正解右值引用只是“接住临时对象的通道”,真正省时间的是在通道里选择“资源交接”而不是“深拷贝”。如果一个类没有堆资源(只有 int、double 等成员),移动和拷贝都是逐字节复制,写移动构造纯属多余。对没有资源可转移的类,加 && 不会带来任何性能变化。

为何常见:把语法特征直接等同于性能优化,忽略了性能的真正来源——省掉深拷贝这个动作本身。

误区forward 和 move 看起来差不多,反正都是把参数变成右值,什么时候用都一样。

正解std::move 无条件把参数转成右值;std::forward 只在你传入的实参原本是右值时转成右值,原本是左值就保持左值。在万能引用模板中,如果该用 forward 的地方用了 move,左值实参也会被强行转成右值,接收端可能误以为可以接管资源,导致意外的资源掏空。forward 只应用于万能引用场景。

为何常见:两个函数名都带“搬运动作”的暗示,签名又相似,模板中混合使用时极易混淆。

面试题

Q1. std::move(x) 在运行时做了什么?

单选题 · easy

  • A.复制 x 的全部数据
  • B.仅为编译期类型转换← 正确答案
  • C.立即释放 x 的内存
  • D.把 x 置为空对象

答案:B。std::move 的本质是 static_cast<T&&>(x),纯编译期类型转换,零运行时开销。它不复制数据、不释放内存、也不置空对象,只是让编译器在重载决议时更倾向于选择移动版本。就像贴一张“可搬运”标签,标签本身不搬家。

Q2. 以下哪个值属于右值(rvalue)?

单选题 · easy

  • A.有名字的局部变量
  • B.全局变量 g 本身
  • C.a + b 的临时结果← 正确答案
  • D.函数引用参数

答案:C。右值是没有持久地址的临时值,a + b 的中间结果在语句结束后销毁,是典型的右值。有名字的局部变量、全局变量、引用参数都能取地址,属于左值。判断口诀:能不能对表达式取地址,能取就是左值。

Q3. 右值引用 T&& 的核心能力是?

单选题 · mid

  • A.只能绑定左值对象
  • B.绑定右值并接管资源← 正确答案
  • C.让基础类型直接加速
  • D.延长所有对象的寿命

答案:B。右值引用的意义在于可以绑定临时对象(右值),从而让移动构造函数“截胡”其资源。它不绑定左值,不能让 int 等基础类型加速,也不会延长所有对象的寿命。它是快速通道,快的是“接管资源”这个动作本身。

Q4. 移动构造函数中,接管资源后必须对源对象做什么?

单选题 · mid

  • A.需要把指针重新分配
  • B.接管后把源指针置空← 正确答案
  • C.复制一份数据做备份
  • D.调用 delete 释放源

答案:B。移动构造把 other 的指针拿过来后,必须把 other.ptr 置为 nullptr。否则两个对象指向同一块内存,析构时会发生 double free 崩溃。置空像搬家后旧家留空书柜,双方各得其所。

Q5. 移动赋值运算符通常要额外处理什么?

单选题 · mid

  • A.检查自赋值并释放旧资源← 正确答案
  • B.修改源对象的 size 大小
  • C.为成员重新分配内存
  • D.把所有成员设为常量

答案:A。移动赋值是“搬进已住人的房子”:先检查 this != &other 防止自毁,再释放当前持有的旧资源防泄漏,然后接管新资源。它不修改源的 size、不重新分配内存、不设常量。这两步是移动赋值区别于移动构造的关键。

Q6. return std::move(x); 的问题在于?

单选题 · mid

  • A.会产生一份多余的拷贝
  • B.可能阻断 NRVO 的省略← 正确答案
  • C.必然导致程序崩溃
  • D.使 x 变成常量对象

答案:B。return x; 时编译器可以做 NRVO,零拷贝零移动;写成 return std::move(x); 后 x 成了右值,编译器不再省略,必须真的调用一次移动构造。放着免费顺风车不坐,非要中途下车再打车。

Q7. 模板推导中,引用折叠 T&& && 的折叠结果是?

单选题 · hard

  • A.折叠结果为 T&
  • B.折叠结果为 T&&← 正确答案
  • C.这是一个语法错误
  • D.结果由运行时决定

答案:B。四条引用折叠规则:T& &、T&& &、T& && 都折叠为 T&,只有 T&& && 折叠为 T&&。折叠发生在编译期,与运行时无关。这条规则支撑了完美转发:左值实参折叠出左值引用,右值实参折叠出右值引用。

Q8. 哪些情况下适合用移动语义提升性能?

多选题 · mid

  • A.把大容器装入 vector 容器← 正确答案
  • B.把临时字符串插入队列← 正确答案
  • C.传递 int 等基础类型参数
  • D.向只读接口传入大对象

答案:AB。把不再用的大对象移入容器、把临时字符串插入队列,都是“整柜搬走”的典型场景,能省掉深拷贝。基础类型拷贝本就是逐字节复制,移动无收益;只读接口用 const& 就够了,不需要也不应该移动。

Q9. 关于编译器复制省略(RVO/NRVO),正确的是?

多选题 · hard

  • A.RVO 是允许的复制省略← 正确答案
  • B.可不调用拷贝或移动构造← 正确答案
  • C.必须手动写 std::move 才触发
  • D.return 局部对象可能被省略← 正确答案

答案:ABD。RVO 和 NRVO 是编译器允许的复制省略,可以完全跳过拷贝/移动构造,直接在调用处构造对象。它不需要 std::move,恰恰相反,std::move 会阻断省略。C++17 起部分场景的复制省略已被标准化为强制行为。

Q10. 关于完美转发,std::forward 的作用是?

多选题 · hard

  • A.按原类别转发左值右值← 正确答案
  • B.无条件转为右值转发
  • C.配合引用折叠保持签名← 正确答案
  • D.等同于无条件 std::move

答案:AC。std::forward 是“保真转发章”,只在实参原本是右值时转成右值,左值保持左值,配合引用折叠保持参数原始类别。它绝不无条件转右值,也不等同于 std::move。中转站原封转发,加急件还是加急件,临时件还是临时件。