C++ 值类别与移动语义详解(精简版)

1) 左值、右值、纯右值(prvalue)、将亡值(xvalue)

面试答案要点

  • 左值(lvalue):在表达式结束后仍然存在、可取地址的对象(有名字或持久存储)。

  • 右值(rvalue):不能持久存在、一般不可取地址的临时值。包含两类:

    • 纯右值(prvalue) :字面量、计算结果等,代表"值";如 42, x + y, std::string("a")(C++17 起 prvalue 直接初始化目标对象)。

    • 将亡值(xvalue) :即将被销毁但仍可"窃取资源"的对象表达式;典型来源 std::move(x)、返回 std::string&& 的函数结果等。

  • 右值引用(T&&) 绑定右值(尤其是 xvalue),为移动语义提供基础。

演示代码

复制代码
#include <iostream>
#include <string>
#include <utility>

int main() {
    std::string s = "hello"; // s 是左值

    auto&& a = std::string("tmp"); // std::string("tmp") 是 prvalue,直接构造成临时对象,再被 a(万能引用)绑定
    auto&& b = std::move(s);       // std::move(s) 是 xvalue(将亡值)

    // std::cout << "s: " << s << "\n"; // 此时 s 的状态未定义,访问它是不安全的,尽管在某些实现中它可能是空的
    std::cout << "a: " << a << "\n";
    std::cout << "b: " << b << "\n"; // b 绑定的是 s 的将亡值(资源移动后 b 拥有)
}

关键点std::move(s) 不是"移动",而是把 s 标记为可被移动(转为 xvalue)

2) T&, const T&, T&& 的绑定规则

面试答案要点(最常考表)

  • T&只能绑定左值。

  • const T&:能绑定左值 也能绑定右值(延长右值临时对象生命周期)。

  • T&&只能绑定右值(prvalue/xvalue)。

  • 模板形参 T&&推导场景 )是转发引用/万能引用

    • 实参是左值 ⇒ T 推导为 T&,形参折叠成 T& &&T&

    • 实参是右值 ⇒ T 推导为 T,形参为 T&&

演示代码

复制代码
#include <iostream>
#include <type_traits>

void bind_l(int& )       { std::cout << "int&\n"; }
void bind_cl(const int& ) { std::cout << "const int&\n"; }
void bind_r(int&& )       { std::cout << "int&&\n"; }

template <typename T>
void probe(T&& x) { // 转发(万能)引用
    if constexpr (std::is_lvalue_reference_v<T&&>) std::cout << "probe: lvalue\n";
    else                                           std::cout << "probe: rvalue\n";
}

int main() {
    int i = 0;
    bind_l(i);       // OK: 左值
    bind_cl(i);      // OK
    // bind_r(i);    // ❌ 左值不能绑定到 int&&
    bind_r(42);      // OK: 右值

    const int ci = 1;
    bind_cl(2);      // OK: 右值绑定 const 引用
    // bind_l(2);    // ❌
    // bind_r(ci);   // ❌ const 左值不是右值

    probe(i);        // lvalue
    probe(3);        // rvalue
}

3) std::movestd::forward 的区别

面试答案要点

  • std::move(x)无条件 把表达式转成 T&&(xvalue)。常用于"我确定要把资源移走"。

  • std::forward<T>(x)有条件 转发:当模板实参 T 为左值引用时保持左值,为非引用时保持右值;用于完美转发

  • 使用场景:

    • 写类的移动构造/赋值 ⇒ 用 std::move

    • 写转发包装器/工厂函数 ⇒ 用 std::forward

演示代码

复制代码
#include <iostream>
#include <string>
#include <utility>

void take(const std::string& s) { std::cout << "take(const&): " << s << "\n"; }
void take(std::string&& s)      { std::cout << "take(&&): "     << s << "\n"; }

template <typename T>
void wrapper_move(T&& x) {
    // 无论传左值还是右值,都被move成右值
    take(std::move(x));
}

template <typename T>
void wrapper_fwd(T&& x) {
    // 保留实参值类别(左值仍左值,右值仍右值)
    take(std::forward<T>(x));
}

int main() {
    std::string s = "hi";
    wrapper_move(s);             // 强行走 && 版本
    wrapper_fwd(s);              // 保持左值 => 走 const&
    wrapper_fwd(std::string("tmp")); // 右值 => 走 &&
}

4) 移动构造 与 拷贝构造 的优先级

面试答案要点

  • 函数重载决议中:能移动就不拷贝(当参数是右值且存在可行移动构造时,会优先选择移动构造)。

  • 若用户显式声明拷贝构造而 声明移动构造,某些情况下编译器不会合成移动构造,导致右值也走拷贝。

  • 为了启用移动,通常显式声明:A(A&&) noexceptA& operator=(A&&) noexcept

  • 对含资源成员(如 std::unique_ptr、容器)移动能显著优化性能。

演示代码

复制代码
#include <iostream>
#include <vector>

struct A {
    std::vector<int> data;

    A() { std::cout << "A()\n"; }
    A(const A&) { std::cout << "A(const A&)\n"; }
    A(A&&) noexcept { std::cout << "A(A&&)\n"; }           // 移动构造
    A& operator=(const A&) { std::cout << "copy=\n"; return *this; }
    A& operator=(A&&) noexcept { std::cout << "move=\n"; return *this; }
};

A makeA() { A a; return a; }

int main() {
    A a1;
    A a2 = makeA();       // C++17 多为 RVO(见下一节);若未RVO,优先调用 A(A&&)
    A a3 = std::move(a1); // 明确右值 => 调用 A(A&&)
}

5) RVO(返回值优化)与移动优化

面试答案要点

  • RVO(Return Value Optimization) :编译器直接在调用方栈/存储上就地构造返回对象,省去拷贝/移动。

  • C++17 保证性 RVOreturn T{...}; / return obj;(局部同名对象)在某些情形下强制省略拷贝/移动(即使定义了移动构造,也不会调用)。

  • 如果不能做(NRVO 失败),才会退回到移动(若可)或拷贝。

  • 实践总结 :写返回值时尽量直接构造返回对象 ,不要为了"优化"而手工 std::move(returnObj)(反而可能禁用 NRVO)。

演示代码(观察是否打印 Move/Copy)

复制代码
#include <iostream>

struct Big {
    Big() { std::cout << "Big()\n"; }
    Big(const Big&) { std::cout << "Copy\n"; }
    Big(Big&&) noexcept { std::cout << "Move\n"; }
};

Big make1() {
    return Big(); // C++17: 保证性RVO,通常不打印 Copy/Move
}

Big make2() {
    Big b;
    return b;     // C++17: 多数实现做 NRVO;若失败,则优先 Move
}

int main() {
    std::cout << "make1 call:\n";
    Big x = make1();
    std::cout << "make2 call:\n";
    Big y = make2();
}

要点 :不要写 return std::move(b);------这会把 b 变成 xvalue,禁用 NRVO,迫使调用移动构造。

小结速背(面试 30 秒版)

  1. 左值可取址、持久存在;右值是临时值。右值含 prvaluexvalue (如 std::move)。

  2. 绑定规则:T& 仅左值;const T& 左/右都行;T&& 仅右值;模板 T&&万能引用

  3. move:无条件右值化;forward:保持实参的值类别(完美转发)。

  4. 右值优先走移动;未声明移动可能退化成拷贝。移动函数请标 noexcept

  5. C++17:RVO/NRVO 常直接省略拷贝/移动;不要 对返回局部用 std::move

相关推荐
脸大是真的好~6 小时前
黑马JAVA+AI 加强14-2 网络编程-UDP和TCP通信-线程和线程池优化通信-BS架构原理
java
金銀銅鐵6 小时前
[Java] 浅析 Map.of(...) 方法和 Map.ofEntries(...) 方法
java·后端
邪恶紫色秋裤6 小时前
解决IntelliJ IDEA控制台输出中文乱码问题
java·ide·乱码·intellij-idea·报错·中文
摇滚侠6 小时前
Spring Boot3零基础教程,StreamAPI 的基本用法,笔记99
java·spring boot·笔记
代码匠心6 小时前
从零开始学Flink:事件驱动
java·大数据·flink·大数据处理
yours_Gabriel6 小时前
【分布式事务】Seata分布式解决方案
java·分布式·微服务
一只游鱼6 小时前
Springboot+BannerBanner(启动横幅)
java·开发语言·数据库
codingPower6 小时前
升级mybatis-plus导致项目启动报错: net.sf.jsqlparser.statement.select.SelectBody
java·spring boot·maven·mybatis
lingran__6 小时前
算法沉淀第十一天(序列异或)
c++·算法