左值右值.

张开发
2026/4/18 8:49:58 15 分钟阅读

分享文章

左值右值.
为什么要有左值引用右值引用有了左值引用为什么还要右值引用这是一个非常深刻的语言设计问题。要回答它需要回到C 追求零开销抽象和避免不必要的拷贝这个核心目标上。一句话回答左值引用是为了避免拷贝传参、返回值右值引用是为了在无法避免拷贝时把拷贝变成“偷”移动语义。一、左值引用解决了什么问题在 C98 时代没有引用时传参和返回都会发生拷贝cpp// 没有引用的时代C语言风格 void func(int* p) { *p 10; } // 必须传指针语法繁琐 // 或者 void func(int p) { p 10; } // 传值修改的是副本外部不变左值引用解决了三个问题1. 避免函数传参时的拷贝cppvoid func(std::string s) { // 传引用不拷贝 s world; } std::string str hello; func(str); // str 被修改且没有拷贝2. 实现函数返回值的“引用传递”cppstd::vectorint vec; int get(int idx) { return vec[idx]; } // 返回引用可以修改 get(0) 100; // 直接修改 vec[0]3. 支持拷贝构造函数深拷贝cppString(const String other); // 参数是 const 左值引用二、左值引用的局限它不能区分“临时对象”左值引用只能绑定到左值有名字、有地址的变量。cppvoid func(std::string s) { ... } std::string str hello; func(str); // ✅ 左值引用绑定到左值 func(hello); // ❌ 错误左值引用不能绑定到临时对象 func(str world); // ❌ 错误表达式结果是临时对象为了解决这个问题C98 提供了const左值引用cppvoid func(const std::string s) { ... } // const 左值引用可以绑定到临时对象 func(hello); // ✅ 可以临时对象的生命周期被延长但这又带来了新问题const意味着你不能修改这个临时对象。即使你知道它是临时的、马上要被销毁的你也无法“偷”它的资源。解决方案右值引用右值引用可以识别出“即将被销毁的对象”临时对象、即将离开作用域的对象然后允许你偷走它的资源。cppclass MyVector { int* data; size_t size; public: // 拷贝构造函数深拷贝 MyVector(const MyVector other) { data new int[other.size]; memcpy(data, other.data, other.size * sizeof(int)); size other.size; } // 移动构造函数浅拷贝 偷资源 MyVector(MyVector other) noexcept { data other.data; // 直接拿过来 size other.size; // 直接拿过来 other.data nullptr; // 让原对象指向空 other.size 0; } };效果MyVector v2 std::move(v1);现在只是交换几个指针O(1) 复杂度什么是移动语义和移动构造区别和右值引用关系右值引用是语法工具移动语义是设计目的移动构造是具体实现。一、三者关系总览先看骨架概念本质一句话解释右值引用语法语言特性用识别“即将消亡的对象”移动语义设计思想意图转移资源所有权而非拷贝移动构造具体代码实现偷走别人资源的构造函数核心关系链C 引入右值引用() 这个语法 → 让我们能写出移动构造/移动赋值这两个函数 → 从而实现了移动语义这个设计目标。二、逐层拆解1. 右值引用 () —— 语法基础作用用来识别“临时对象”或“即将被销毁的对象”。cppvoid func(int a) { cout 左值引用\n; } // 只能接收左值变量 void func(int a) { cout 右值引用\n; } // 只能接收右值临时值 int main() { int x 10; func(x); // 调用 int 版本 func(10); // 调用 int 版本10 是临时值 func(std::move(x)); // 调用 int 版本std::move 把左值转成右值 }为什么需要它没有之前无法在函数参数层面区分“这个对象是否即将销毁”有了我们就知道这个对象反正要死了可以偷它的资源2. 移动语义 —— 设计目的核心思想转移资源所有权而不是复制资源。cpp// 没有移动语义的时代C98 vectorint a {1,2,3,4,5}; vectorint b a; // 只有拷贝必须复制5万个数据如果很大就很慢 // 有移动语义的时代C11 vectorint a {1,2,3,4,5}; vectorint b std::move(a); // 移动只是交换指针O(1) 复杂度 // 移动后 a 变空b 接管了资源效果把 O(n) 的拷贝变成 O(1) 的指针交换。3. 移动构造 —— 具体实现定义参数为的构造函数负责实现移动语义。cppclass MyString { char* data; public: // 移动构造函数 MyString(MyString other) noexcept : data(other.data) // 1. 偷走指针 { other.data nullptr; // 2. 让对方指向空防止 double delete } // 拷贝构造函数对比 MyString(const MyString other) { data new char[strlen(other.data) 1]; strcpy(data, other.data); // 深拷贝慢 } };关键点参数必须是右值引用要“偷”资源并让原对象“置空”通常标记为noexcept保证不抛异常三、深度对比移动语义 vs 移动构造维度移动语义移动构造层次概念层What实现层How作用范围整个语言特性某个类的具体函数表现形式一种编程范式ClassName(ClassName)是否唯一移动语义还可以通过移动赋值实现移动构造只是其中一种实现方式依赖关系依赖右值引用语法是实现移动语义的一种手段补充说明移动语义不止移动构造还包括移动赋值运算符cppclass MyString { public: // 移动构造 MyString(MyString other); // 移动赋值也是移动语义的一部分 MyString operator(MyString other); };四、完整代码示例三者联动cpp#include iostream #include vector class Buffer { int* ptr; size_t size; public: // 构造 Buffer(size_t s) : size(s), ptr(new int[s]) { std::cout 构造\n; } // 拷贝构造深拷贝 Buffer(const Buffer other) : size(other.size), ptr(new int[other.size]) { std::copy(other.ptr, other.ptr size, ptr); std::cout 拷贝构造深拷贝\n; } // 移动构造移动语义的实现⭐ Buffer(Buffer other) noexcept : ptr(other.ptr), size(other.size) { other.ptr nullptr; other.size 0; std::cout 移动构造偷资源\n; } ~Buffer() { delete[] ptr; std::cout 析构\n; } }; int main() { Buffer a(1000000); // 构造 Buffer b a; // 拷贝构造慢 Buffer c std::move(a); // 移动构造快 // 移动语义让 c 偷走 a 的资源 // 右值引用std::move(a) 把 a 转成右值触发移动构造 // 移动构造Buffer(Buffer) 负责执行偷窃 return 0; }输出text构造 拷贝构造深拷贝 移动构造偷资源 析构 // c 析构 析构 // b 析构 析构 // a 析构a 已经是空指针delete nullptr 安全五、常见误区澄清❌ 误区1“移动语义就是 std::move”错。std::move只是把左值转换成右值引用的语法糖它本身不移动任何东西。真正的移动发生在移动构造/移动赋值中。cppstd::vectorint a, b; std::move(a); // 什么都没发生只是返回一个右值引用 b std::move(a); // 移动赋值才真正发生移动❌ 误区2“移动后原对象就不能用了”不完全对。移动后的对象处于“有效但未指定”的状态你可以给它赋新值但不能再假设它原来的内容。cppstd::vectorint v1 {1,2,3}; std::vectorint v2 std::move(v1); // v1 现在可能是空的但你不能直接使用 v1[0] v1 {4,5,6}; // ✅ 可以重新赋值后使用❌ 误区3“所有类型移动都比拷贝快”不一定。对于int、char等基础类型移动和拷贝一样快都是直接复制值。移动的优势体现在管理堆内存的类如string、vector。六、总结一张图text┌──────────────────────────────────────────────────┐ │ C 设计目标 │ │ 实现高效的资源转移移动语义 │ └──────────────────────────────────────────────────┘ ↓ 需要语法支持 ┌──────────────────────────────────────────────────┐ │ 语言特性右值引用 () │ │ 用来识别“即将消亡的对象” │ └──────────────────────────────────────────────────┘ ↓ 具体实现 ┌──────────────────────────────────────────────────┐ │ 移动构造函数 / 移动赋值运算符 │ │ 参数是 负责“偷”资源 │ └──────────────────────────────────────────────────┘一句话终极总结移动语义是想做的事偷资源右值引用是识别谁可以被偷的语法移动构造是实际执行偷窃的代码。简单记忆右值引用 认识“临时对象”这个标签移动语义 “资源可以偷”这个想法移动构造 “我动手去偷”这个动作什么是完美转发为什么要保持原有左右值属性这是一个非常深刻的问题。要理解为什么要保持原有属性得先明白如果不保持会发生什么。一句话回答完美转发是为了在函数层层传递参数时保留参数的“左右值属性”从而让移动语义能够按预期生效。一、先看一个痛点不保持属性的后果假设你写了一个工厂函数想把参数转发给构造函数cpp// 目标类 struct Person { std::string name; // 构造函数重载 Person(const std::string n) : name(n) { // 左值版本拷贝 std::cout 拷贝构造\n; } Person(std::string n) : name(std::move(n)) { // 右值版本移动 std::cout 移动构造\n; } };❌ 错误的转发没有完美转发cpptemplatetypename T Person createPerson(T arg) { // 传值 return Person(arg); // arg 在这里永远是左值 } std::string s Alice; auto p1 createPerson(s); // 期望拷贝实际拷贝 ✅ auto p2 createPerson(Bob); // 期望移动实际拷贝 ❌ 问题 auto p3 createPerson(std::move(s)); // 期望移动实际拷贝 ❌ 问题问题出在哪createPerson(Bob)传入的是右值临时字符串但参数arg是一个有名字的变量在函数内部它变成了左值调用Person(arg)时永远匹配到const std::string拷贝版本移动语义失效了二、为什么会丢失右值属性C 的规则有名字的变量都是左值。cppvoid func(std::string s) { // s 的类型是右值引用 // 但 s 本身有名字所以 s 是一个左值 another_func(s); // 这里 s 被当作左值传递 } func(hello); // 传入的是右值但进入函数后变成了左值核心矛盾T可以绑定到右值但一旦绑定了这个参数变量本身是左值因为它有名字、有地址三、完美转发的解决方案std::forwardcpptemplatetypename T Person createPerson(T arg) { // 万能引用 return Person(std::forwardT(arg)); // 完美转发 }std::forwardT(arg)的作用如果arg原本是右值就把它转回右值如果arg原本是左值就保持左值cppstd::string s Alice; createPerson(s); // T std::string, forward 返回左值 createPerson(Bob); // T std::string, forward 返回右值 createPerson(std::move(s)); // T std::string, forward 返回右值四、为什么“保持原有属性”这么重要场景1移动语义的传递cpp// 一个更实际的例子vector 的 emplace_back templatetypename... Args void vectorT::emplace_back(Args... args) { // 必须完美转发否则构造时永远拷贝 new (ptr) T(std::forwardArgs(args)...); } // 用户代码 std::vectorPerson vec; std::string name Alice; vec.emplace_back(name); // 拷贝正确 vec.emplace_back(Bob); // 移动正确不拷贝 vec.emplace_back(std::move(name)); // 移动正确如果不完美转发所有参数都变成左值Bob这种临时字符串也会被拷贝性能损失巨大。场景2智能指针的工厂函数cpptemplatetypename T, typename... Args std::unique_ptrT make_unique(Args... args) { // 必须完美转发否则参数被拷贝 return std::unique_ptrT(new T(std::forwardArgs(args)...)); } auto p make_uniquePerson(Alice); // 移动构造不是拷贝场景3包装器/代理模式cpptemplatetypename Func, typename... Args auto wrapper(Func f, Args... args) { // 执行前做一些事... return std::forwardFunc(f)(std::forwardArgs(args)...); // 执行后做一些事... }五、完美转发的完整原理引用折叠规则实际类型T推导结果T实际类型左值intintint左值引用右值intintint右值引用std::forward的简化实现cpptemplatetypename T T forward(typename remove_referenceT::type arg) { return static_castT(arg); }效果如果T是int左值返回int左值引用如果T是int右值返回int右值引用六、不完美转发 vs 完美转发对比调用方式不完美转发完美转发func(lvalue)拷贝拷贝 ✅func(rvalue)拷贝 ❌ 性能损失移动 ✅func(std::move(x))拷贝 ❌ 性能损失移动 ✅临时对象拷贝 ❌ 性能损失移动 ✅性能差异对于std::string这种类型拷贝是 O(n)移动是 O(1)。对于大对象差距巨大。七、总结为什么要保持原有左右值属性因为右值是“可以偷的资源”左值是“不能偷的资源”。如果丢失了右值属性就会失去移动的机会导致不必要的拷贝。完美转发的本质组件作用T万能引用接收任意类型参数保留原始信息引用折叠让T能推导出正确的类型std::forward根据T的类型恢复参数的左右值属性一句话记忆完美转发 万能引用 引用折叠 forward目的是让参数在层层传递中“不忘本”。没有完美转发泛型代码中的移动语义就会失效C11 引入的移动语义就只能在局部使用无法在函数间传递。

更多文章