【C++ 面试真题】聊聊 C++ 的移动语义与右值引用
【C 面试真题】聊聊 C 的移动语义与右值引用上篇讲了拷贝——“复制一份房子”。可如果源对象马上就要没了比如函数返回的临时对象再老老实实盖一栋新房子、把家具一件件搬过去纯属浪费。C11 的移动语义就是来偷资源的直接把源对象的内部指针搬过来源置空O(1) 完成。本文讲清右值引用、std::move的真相、为什么移动要noexcept还有完美转发的概念。一、先说结论移动 “偷资源不复制”❓ 移动语义到底解决了什么问题✅ 一句话让从临时对象身上转移资源变得既合法又廉价——把昂贵的深拷贝变成 O(1) 的指针搬家。对比拷贝和移动structBuffer{int*data_;intn_;// 拷贝构造分配 复制O(n)Buffer(constBuffero);// 移动构造偷指针O(1)Buffer(Buffero)noexcept;};Buffermake(intn){returnBuffer(n);// 返回一个临时对象}Buffer bmake(1000);// 没有移动深拷贝 1000 个 int// 有移动偷指针瞬间完成 ✅思想临时对象反正在下一行就析构了它的资源借给新对象比复制一份划算得多。移动语义就是给这种借提供合法途径。二、什么是左值 / 右值❓ 经常听到右值引用到底什么是右值✅ 精简版口诀左值lvalue有名字、能取地址、能反复用。比如变量x。右值rvalue临时的、没名字、用完就没了。比如字面量42、表达式x1、函数返回的临时对象。intx1;int*px;// ✅ x 是左值能取地址// int* q (x1); // ❌ x1 是右值取不到地址intrx1;// ✅ 绑右值// int r2 x1; // ❌ 普通引用绑不了右值引用体系引用类型能绑什么写法非常量左值引用只左值T常量左值引用左值 右值const T右值引用只右值T⚠️别钻牛角尖C11 其实把右值细分成 prvalue、xvalue 等合称 value category面试一般不深挖。记住能取地址左值临时将亡右值就够用了。三、移动构造与移动赋值怎么写❓ 移动构造/赋值具体长什么样✅ 把源对象的资源偷过来把源指针赋给自己再把源指针置空防止源析构时把资源 delete 掉。structBuffer{Buffer(intn):data_(newint[n]),n_(n){}// 移动构造偷Buffer(Buffero)noexcept:data_(o.data_),n_(o.n_){o.data_nullptr;// ✅ 关键源置空o.n_0;}// 移动赋值Bufferoperator(Buffero){if(thiso)return*this;delete[]data_;// 释放旧的data_o.data_;// 偷n_o.n_;o.data_nullptr;o.n_0;return*this;}~Buffer(){delete[]data_;}int*data_;intn_;};对比拷贝和移动操作拷贝移动复杂度O(n)逐个复制O(1)换指针源对象不变变成空壳资源归属两份各持一份转移到目标四、std::move到底干了什么❓std::move是不是真的移动了对象✅不是这是最常见的误解。std::move什么都不移动它只是一个类型转换cast——把一个左值强制伪装成右值好让重载决议选到移动构造/赋值。templateclassTTmove(Tx)noexcept{returnstatic_castT(x);}真正干活的是移动构造或移动赋值。std::move只是发了个我允许你被偷的信号。Buffera(100);Buffer ba;// 拷贝a 还是左值Buffer cstd::move(a);// 移动a 被偷了// 之后 a 已是空壳别再用 a.data_⚠️关键认知std::move无开销、不分配、不复制、不调用任何东西——它纯粹是个编译期的类型转换。如果对应类型没有移动构造std::move就退化成拷贝。自测题std::move(const T)会发生什么答案源是const匹配不到T移动要求非常量重载会退到const T的拷贝构造——给 const 对象std::move等于没 move。这是真实项目里常见的以为移动了其实拷贝了的坑。五、为什么移动构造要noexcept核心考点❓ 移动构造为什么要加noexcept不加会怎样✅不加noexcept很多场景下移动根本不会被触发——容器会退回拷贝。最经典的就是std::vector扩容。原理vector扩容时要把老元素搬到新内存。搬的时候它有选择元素的移动构造是noexcept→安全搬直接移动快否则 → 为保强异常保证搬一半失败要能回滚退回拷贝慢但拷贝失败可回滚。structBad{Bad(Bad);// ❌ 没 noexcept};structGood{Good(Good)noexcept;// ✅};std::vectorBadv1;v1.push_back(Bad{});// 扩容时 → 拷贝std::vectorGoodv2;v2.push_back(Good{});// 扩容时 → 移动 ✅结论凡是偷资源、不抛异常的移动构造/赋值一律加noexcept。这不是优化而是能不能用上移动的前提。面试官最爱问这个。六、移动后源对象什么状态❓std::move(a)之后a还能用吗✅ 标准的规定是valid but unspecified有效但未指定——也就是能用但别假设里面还有什么内容。具体说移动后a仍然是个合法对象能正常析构、能被赋新值但内部值未知可能是空可能不是只能调用不依赖具体值的操作赋值a ...、析构。string shello;string tstd::move(s);// s 状态未指定但合法sworld;// ✅ 允许couts[0];// ❌ 别假设 s 还有内容⚠️别从源对象身上读但写一个新值进去是安全的。日常代码里std::move之后尽量别再碰那个源对象。七、完美转发简介❓ 听说过完美转发是干什么的✅ 一句话让函数模板把参数原封不动地传给下一个函数——保持它的左值/右值属性、const 属性不变。templateclassTvoidwrapper(Tx){// 在模板里是转发引用target(std::forwardT(x));// 原样转发}wrapper(左值)→target收到左值引用wrapper(右值)→target收到右值引用触发移动。为什么要它写库的人经常要做转发壳——比如make_unique、emplace_back、各种工厂函数它们自己不构造对象只是把参数递给真正的构造函数。如果不会完美转发递一个右值进去、里面按左值拷贝了性能就白丢。 这里只点个概念。完美转发在代码复用/库设计里是大杀器——写自己的容器、工厂、装饰器都靠它那是另一个大话题。八、Rule of Five 再回顾❓ 加上移动后三五法则变成什么样✅Rule of Three析构 拷贝构造 拷贝赋值再加上移动构造、移动赋值两个就是Rule of FivestructBuffer{~Buffer();// 1. 析构Buffer(constBuffer);// 2. 拷贝构造Bufferoperator(constBuffer);// 3. 拷贝赋值Buffer(Buffer)noexcept;// 4. 移动构造Bufferoperator(Buffer);// 5. 移动赋值};⚠️常见错误写了移动构造以为可以不用写拷贝构造。恰恰相反——C 规定一旦你声明了移动构造编译器就不再合成拷贝构造。所以要么五个都写要么用 RAII 成员实现Rule of Zero容器/智能指针会自动支持移动连写都不用写。九、面试高频追问❓ Q1std::move之后那个对象还能用吗✅ 处于有效但未指定状态——能赋值、能析构但别读它的值。最佳实践是 move 完就别再碰。❓ Q2什么类型的std::move其实等于没 move✅没有移动构造的类型比如只写了个默认拷贝的旧类、const 对象std::move(const T)退化成拷贝、基本类型int/double 没有资源可偷move 就是复制。❓ Q3返回局部对象时该不该写return std::move(t);✅千万别这是经典坑。局部对象返回时本可以触发NRVO直接在调用方构造零拷贝你std::move一下反而把 NRVO 关掉了强制走一次移动构造。return t;永远比return std::move(t);好编译器会自动判断该不该移动。❓ Q4移动构造里抛异常会怎样✅ 灾难。移动到一半抛异常目标对象不完整、源对象已被偷空——两边都坏了没法回滚。所以移动构造必须 noexcept——偷资源这种操作本来就不该失败。❓ Q5T一定是右值引用吗✅在模板里不是模板里的T叫转发引用forwarding reference能同时绑左值和右值通过引用折叠推导。只有在非模板、类型确定的场合如Buffer才是纯右值引用。这点完美转发时最绕。十、总结速查表考点一句话结论移动的本质偷指针O(1)源置空左值/右值能取地址左值临时将亡右值std::move只是 cast不移动任何东西const对象 move退化成拷贝移动构造 noexceptvector 扩容要用必须加移动后源状态有效但未指定别读完美转发Tstd::forward原样转发Rule of Five三大法则 移动构造 移动赋值返回局部对象别std::move会挡 NRVO一句话回顾移动是偷资源std::move只是发允许偷的信号偷资源的移动构造必须noexcept——否则连vector扩容都不会用它。返回局部对象时别画蛇添足加std::move编译器比你想得聪明。如果您觉得本篇内容对你有帮助欢迎点赞 、收藏 ⭐、转发 。到这里构造、析构、拷贝、移动这对象生命周期四件套就齐了。下期我们正式聊面向对象的重头戏——虚函数与多态虚函数表长什么样、对象内存里存了什么、构造析构里调虚函数会发生什么敬请关注