news 2026/5/7 10:11:38

左值右值.

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
左值右值.

为什么要有左值引用,右值引用,有了左值引用为什么还要右值引用

这是一个非常深刻的语言设计问题。要回答它,需要回到C++ 追求零开销抽象避免不必要的拷贝这个核心目标上。

一句话回答:

左值引用是为了避免拷贝(传参、返回值),右值引用是为了在无法避免拷贝时,把拷贝变成“偷”(移动语义)。


一、左值引用解决了什么问题?

在 C++98 时代,没有引用时,传参和返回都会发生拷贝:

cpp

// 没有引用的时代(C语言风格) void func(int* p) { *p = 10; } // 必须传指针,语法繁琐 // 或者 void func(int p) { p = 10; } // 传值,修改的是副本,外部不变

左值引用&解决了三个问题

1. 避免函数传参时的拷贝

cpp

void func(std::string& s) { // 传引用,不拷贝 s += " world"; } std::string str = "hello"; func(str); // str 被修改,且没有拷贝

2. 实现函数返回值的“引用传递”

cpp

std::vector<int> vec; int& get(int idx) { return vec[idx]; } // 返回引用,可以修改 get(0) = 100; // 直接修改 vec[0]

3. 支持拷贝构造函数(深拷贝)

cpp

String(const String& other); // 参数是 const 左值引用

二、左值引用的局限:它不能区分“临时对象”

左值引用只能绑定到左值(有名字、有地址的变量)。

cpp

void func(std::string& s) { ... } std::string str = "hello"; func(str); // ✅ 左值引用绑定到左值 func("hello"); // ❌ 错误!左值引用不能绑定到临时对象 func(str + "world"); // ❌ 错误!表达式结果是临时对象

为了解决这个问题,C++98 提供了const左值引用:

cpp

void func(const std::string& s) { ... } // const 左值引用可以绑定到临时对象 func("hello"); // ✅ 可以!临时对象的生命周期被延长

但这又带来了新问题const意味着你不能修改这个临时对象。即使你知道它是临时的、马上要被销毁的,你也无法“偷”它的资源。

解决方案:右值引用

右值引用&&可以识别出“即将被销毁的对象”(临时对象、即将离开作用域的对象),然后允许你偷走它的资源。

cpp

class 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. 右值引用 (&&) —— 语法基础

作用:用来识别“临时对象”或“即将被销毁的对象”。

cpp

void 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

// 没有移动语义的时代(C++98) vector<int> a = {1,2,3,4,5}; vector<int> b = a; // 只有拷贝,必须复制5万个数据(如果很大就很慢) // 有移动语义的时代(C++11) vector<int> a = {1,2,3,4,5}; vector<int> b = std::move(a); // 移动,只是交换指针,O(1) 复杂度 // 移动后 a 变空,b 接管了资源

效果:把 O(n) 的拷贝变成 O(1) 的指针交换。


3. 移动构造 —— 具体实现

定义:参数为&&的构造函数,负责实现移动语义。

cpp

class 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&&)
是否唯一移动语义还可以通过移动赋值实现移动构造只是其中一种实现方式
依赖关系依赖右值引用语法是实现移动语义的一种手段

补充说明:移动语义不止移动构造,还包括移动赋值运算符

cpp

class 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只是把左值转换成右值引用的语法糖,它本身不移动任何东西。真正的移动发生在移动构造/移动赋值中。

cpp

std::vector<int> a, b; std::move(a); // 什么都没发生,只是返回一个右值引用 b = std::move(a); // 移动赋值才真正发生移动

❌ 误区2:“移动后原对象就不能用了”

不完全对。移动后的对象处于“有效但未指定”的状态,你可以给它赋新值,但不能再假设它原来的内容。

cpp

std::vector<int> v1 = {1,2,3}; std::vector<int> v2 = std::move(v1); // v1 现在可能是空的,但你不能直接使用 v1[0] v1 = {4,5,6}; // ✅ 可以重新赋值后使用

❌ 误区3:“所有类型移动都比拷贝快”

不一定。对于intchar等基础类型,移动和拷贝一样快(都是直接复制值)。移动的优势体现在管理堆内存的类(如stringvector)。


六、总结一张图

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"; } };

❌ 错误的转发(没有完美转发)

cpp

template<typename 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++ 的规则:有名字的变量都是左值

cpp

void func(std::string&& s) { // s 的类型是右值引用 // 但 s 本身有名字,所以 s 是一个左值! another_func(s); // 这里 s 被当作左值传递 } func("hello"); // 传入的是右值,但进入函数后变成了左值

核心矛盾

  • T&&可以绑定到右值

  • 但一旦绑定了,这个参数变量本身是左值(因为它有名字、有地址)


三、完美转发的解决方案:std::forward

cpp

template<typename T> Person createPerson(T&& arg) { // 万能引用 return Person(std::forward<T>(arg)); // 完美转发 }

std::forward<T>(arg)的作用

  • 如果arg原本是右值,就把它转回右值

  • 如果arg原本是左值,就保持左值

cpp

std::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 template<typename... Args> void vector<T>::emplace_back(Args&&... args) { // 必须完美转发,否则构造时永远拷贝 new (ptr) T(std::forward<Args>(args)...); } // 用户代码 std::vector<Person> vec; std::string name = "Alice"; vec.emplace_back(name); // 拷贝(正确) vec.emplace_back("Bob"); // 移动(正确!不拷贝) vec.emplace_back(std::move(name)); // 移动(正确!)

如果不完美转发:所有参数都变成左值,"Bob"这种临时字符串也会被拷贝,性能损失巨大。

场景2:智能指针的工厂函数

cpp

template<typename T, typename... Args> std::unique_ptr<T> make_unique(Args&&... args) { // 必须完美转发,否则参数被拷贝 return std::unique_ptr<T>(new T(std::forward<Args>(args)...)); } auto p = make_unique<Person>("Alice"); // 移动构造,不是拷贝

场景3:包装器/代理模式

cpp

template<typename Func, typename... Args> auto wrapper(Func&& f, Args&&... args) { // 执行前做一些事... return std::forward<Func>(f)(std::forward<Args>(args)...); // 执行后做一些事... }

五、完美转发的完整原理

引用折叠规则

实际类型T推导结果T&&实际类型
左值int&int&int&(左值引用)
右值int&&intint&&(右值引用)

std::forward的简化实现

cpp

template<typename T> T&& forward(typename remove_reference<T>::type& arg) { return static_cast<T&&>(arg); }

效果

  • 如果Tint&(左值),返回int&(左值引用)

  • 如果Tint(右值),返回int&&(右值引用)


六、不完美转发 vs 完美转发对比

调用方式不完美转发完美转发
func(lvalue)拷贝拷贝 ✅
func(rvalue)拷贝 ❌ 性能损失移动 ✅
func(std::move(x))拷贝 ❌ 性能损失移动 ✅
临时对象拷贝 ❌ 性能损失移动 ✅

性能差异:对于std::string这种类型,拷贝是 O(n),移动是 O(1)。对于大对象,差距巨大。


七、总结

为什么要保持原有左右值属性?

因为右值是“可以偷的资源”,左值是“不能偷的资源”。如果丢失了右值属性,就会失去移动的机会,导致不必要的拷贝。

完美转发的本质

组件作用
T&&(万能引用)接收任意类型参数,保留原始信息
引用折叠T&&能推导出正确的类型
std::forward根据T的类型,恢复参数的左右值属性

一句话记忆

完美转发 = 万能引用 + 引用折叠 + forward,目的是让参数在层层传递中“不忘本”。

没有完美转发,泛型代码中的移动语义就会失效,C++11 引入的移动语义就只能在局部使用,无法在函数间传递。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/7 10:09:56

VMware虚拟机中Ubuntu 20.04环境配置全攻略

1. 准备工作&#xff1a;软硬件环境检查 在开始配置Ubuntu 20.04虚拟机之前&#xff0c;建议先花5分钟检查基础环境。我遇到过不少因为版本不匹配导致安装失败的案例&#xff0c;这里分享几个关键检查点&#xff1a; 首先是VMware版本选择。目前主流使用VMware Workstation 16.…

作者头像 李华
网站建设 2026/4/17 18:08:23

终极ECAPA-TDNN说话人识别系统:从零到工业级部署的完整指南

终极ECAPA-TDNN说话人识别系统&#xff1a;从零到工业级部署的完整指南 【免费下载链接】ECAPA-TDNN Unofficial reimplementation of ECAPA-TDNN for speaker recognition (EER0.86 for Vox1_O when train only in Vox2) 项目地址: https://gitcode.com/gh_mirrors/ec/ECAPA…

作者头像 李华
网站建设 2026/4/17 17:05:30

便携式土壤多参数测定仪

全能型土壤多参数测定仪&#xff0c;凭借便携性、高精度、全能检测的优势&#xff0c;广泛适配各类土壤监测场景&#xff0c;尤其适合野外作业&#xff0c;具体适配场景如下&#xff1a;野外作业场景&#xff1a;体积小巧、便于携带&#xff0c;双电源续航持久&#xff0c;可在…

作者头像 李华