左值右值与移动语义
左值右值与移动语义面试题 1左值和右值的本质区别是什么#includecstringinta42;// a 是左值42 是右值int*pa;// OK可以取 a 的地址// int* q 42; // 编译错误不能取右值的地址能取地址的是左值lvalue不能取地址的是右值rvalue。右值是即将销毁的临时对象可以安全地偷走它的资源。intx10;// x 是左值intyx5;// (x 5) 是右值运算结果是一个临时量intrref42;// 右值引用可以绑定右值// int ref 42; // 编译错误非常量左值引用不能绑右值constintcref42;// OKconst 左值引用可以绑右值延长生命周期面试追问具名右值引用本身是左值还是右值voidfoo(intarg){// arg 是 int 类型// 但 arg 本身是一个具名变量是左值// int another arg; // 编译错误arg 是左值不能绑到右值引用}面试题 2std::move 到底做了什么templatetypenameTconstexprstd::remove_reference_tTmove(Tt)noexcept{returnstatic_caststd::remove_reference_tT(t);}std::move 本质就是一个 static_castT不产生任何运行时代码。它只是告诉编译器这个对象可以移动让重载决议选择移动版本的函数。#includeiostream#includestring#includevectorstructBuffer{char*data;size_t size;Buffer(size_t n):size(n),data(newchar[n]){std::coutConstructor\n;}~Buffer(){delete[]data;}Buffer(constBufferother):size(other.size),data(newchar[other.size]){std::memcpy(data,other.data,size);std::coutCopy constructor\n;}Buffer(Bufferother)noexcept:size(other.size),data(other.data){other.datanullptr;other.size0;std::coutMove constructor\n;}};intmain(){Buffera(1024);Buffer bstd::move(a);// 调用移动构造// 此时 a.data nullptr, a 处于空状态}移动构造/赋值必须标记 noexcept否则std::vector扩容时会退化回拷贝#includeiostream#includevectorstructMayThrow{MayThrow()default;MayThrow(MayThrow){/* 没写 noexcept */}};structNoThrow{NoThrow()default;NoThrow(NoThrow)noexceptdefault;};intmain(){std::vectorMayThrowv1;v1.reserve(1);v1.emplace_back();// 再次插入触发扩容因为移动不保证 noexceptvector 用拷贝v1.emplace_back();// 调用拷贝而非移动std::vectorNoThrowv2;v2.reserve(1);v2.emplace_back();v2.emplace_back();// 调用移动O(1) 指针交换}std::vector::reserve之后扩容时如果类型提供了noexcept移动构造就用移动常数时间否则用拷贝线性时间。这是 C 标准库的强异常安全保证。面试题 3引用折叠四条规则当模板参数 T 推导为引用类型时T的行为由引用折叠reference collapsing确定T 被推导为T 实际类型规则TTT → TT TT → TT TT → TT TT → T核心结论只要出现左值引用结果就是左值引用只有全是右值引用 才得到右值引用。T TT TT TT TtemplatetypenameTvoidfoo(Tt){// 万能引用forwarding reference// T 的推导结果决定 t 的类别}intmain(){intx0;foo(x);// T int ⇒ T int ⇒ 左值引用foo(0);// T int ⇒ T int ⇒ 右值引用}面试追问为什么模板中的 T 叫万能引用而写死的 int 不是答只有发生在模板参数推导或 auto 场景中的T才是万能引用前向引用会根据实参自动折叠。显式写int永远只是一个右值引用不能绑定左值。面试题 4std::forward 与完美转发原理完美转发perfect forwarding的目标将参数原样包括值类别传递给下一个函数。#includeiostream#includestring#includeutilityvoidprocess(intx){std::coutLvalue\n;}voidprocess(intx){std::coutRvalue\n;}templatetypenameTvoidrelay(Tt){process(std::forwardT(t));}intmain(){inta0;relay(a);// 输出 Lvaluerelay(42);// 输出 Rvalue}std::forward的实现原理templatetypenameTconstexprTforward(std::remove_reference_tTt)noexcept{returnstatic_castT(t);}templatetypenameTconstexprTforward(std::remove_reference_tTt)noexcept{static_assert(!std::is_lvalue_reference_vT);returnstatic_castT(t);}当T int时forwardint返回int → 折叠为int左值转发。当T int时forwardint返回int右值转发。对比 std::move 和 std::forward函数行为典型场景move(t)无条件转右值引用明确要转移资源forwardT(t)保真传递值类别模板中转发参数给下一层面试题 5push_back 与 emplace_back 的区别#includeiostream#includestring#includevectorstructExpensive{std::string name;explicitExpensive(std::string s):name(std::move(s)){std::cout构造\n;}Expensive(constExpensive){std::cout拷贝\n;}Expensive(Expensive)noexcept{std::cout移动\n;}};intmain(){std::vectorExpensivev;v.reserve(10);std::cout--- push_back 传入临时对象 ---\n;v.push_back(Expensive(a));// 构造 移动std::cout--- emplace_back 直接构造 ---\n;v.emplace_back(b);// 只构造无移动/拷贝}emplace_back 直接在 vector 分配的内存上构造对象传的是构造参数push_back 传的是已构造好的对象需要构造一次 移动/拷贝一次。C17 起push_back也有右值重载但 emplace_back 在传入多个参数或不想写临时对象时更优。面试题 6RVO / NRVO / Guaranteed Copy ElisionRVOReturn Value Optimization编译器在调用方栈帧上直接构造返回值省略拷贝/移动#includeiostreamstructBig{Big(){std::cout构造\n;}Big(constBig){std::cout拷贝\n;}Big(Big)noexcept{std::cout移动\n;}};BigmakeRVO(){returnBig{};// 临时对象 —— RVO 消除拷贝}intmain(){std::cout--- RVO ---\n;Big bmakeRVO();// 只输出一次构造}NRVONamed RVO对具名变量做返回值优化但编译器不强制BigmakeNRVO(){Big local;// 具名对象// 一些操作...returnlocal;// NRVO编译器尝试省略拷贝/移动}C17 Guaranteed Copy ElisionC17 起prvalue纯右值不再触发拷贝/移动语言层面保证省略#includeiostreamstructGuaranteed{Guaranteed(){std::cout构造\n;}Guaranteed(constGuaranteed)delete;// 拷贝删除Guaranteed(Guaranteed)delete;// 移动也删除};intmain(){Guaranteed gGuaranteed{};// C17 OKprvalue 直接构造// C14 中不行需要可拷贝/移动}C17 guaranteed copy elision 的前提是从 prvalue 初始化。而 NRVO 不是强制性的所以即使 C17 也不能保证 NRVO。完整速查表概念一句话总结左值有地址、有名字取址合法右值临时对象、字面量取址不合法std::movestatic_castT只标记、不移动std::forward根据模板参数保真传递值类别T模板中万能引用自动折叠引用折叠有 就 只有 才是 movenoexcept必须写否则 vector 扩容退化为拷贝emplace_back原地构造零额外移动/拷贝RVO编译器跳过拷贝直接构造到返回位置C17 保证省略prvalue 初始化零拷贝语言级承诺