【C++】对左值引用右值引用的深入理解(右值引用与移动语义)
🌈 个人主页:谁在夜里看海.
🔥 个人专栏:《C++系列》《Linux系列》
⛰️ 天高地阔,欲往观之。
目录
前言:对引用的底层理解
一、左值与右值
提问:左值在左,右值在右?
二、左值引用与右值引用
1.提问:右值引用为左值?
2.不能取地址≠没有地址
3.左右值引用的绑定
4.左右值引用的比较
三、右值引用的意义
1.左值引用的使用场景
作为函数参数
作为函数返回值
2.左值引用的局限
3.右值引用和移动语义
前言:对引用的底层理解
在区分左右值引用之前,我先补充一下对引用的理解。
相较于C语言,C++引入了一种语法:引用,我们需要了解的是,为什么C语言没有引用,而C++有呢?
在C语言中,设计者希望语言保持简单并且支持直接操作内存,因此选择使用指针完成数据的传递,通过指针,C语言可以实现对变量的直接访问和修改;
在C++中,引入了更高级抽象机制,引用作为一种高级抽象比指针更安全、易用,并且在实现参数传递和返回值时不需要&、*操作符,更符合直观语义,便于面向对象编程。
引用被看作一种别名,在上层,是变量的别名,但在底层,其实是地址的别名,为什么这么说呢:
在语言层面,int num = 10;表示创建一个int类型的变量num,并初始化为10:
但是跳出高级语言层面,我们来看底层:num并不是什么变量名称,num对应了一个地址,是一个位于进程地址空间栈区的地址;int也不是什么类型,它表示从该地址往后的4字节的空间被进程使用了,要以4字节为一个整体,修改该地址上的内容;而字面常量10呢,用二进制表示00001010,根据辅助对象的不同进行提升或截断,10要赋值给int对象,先被提升成32bit即4字节,存储时将这些字面值从正文代码区拷贝到num对应的栈区地址上。
所以,引用实际上是地址的别名,是与地址建立的一种映射关系,我们可以通过不同的别名访问同一块地址空间,由于引用并不直接接触地址,这使得程序出错的可能性减少,安全性也提高了。
说完了引用,我们来说一下左值右值:
一、左值与右值
左值与右值统称为值类别,它们都是表示数据的表达式,而左右的区分决定了表达式的使用方式,为什么这么说呢?下面来左值右值的特点就知道了:
左值:可以获取地址,并且可以对其赋值。如变量名、数组指针等
int a = 10; // a 是左值,&a 有效
a = 15; // 可以对左值进行赋值
int arr[5] = {1, 2, 3, 4, 5};
arr[2] = 10; // arr[2] 是左值,可以被赋值class MyClass {
public:int value;
};MyClass obj;
obj.value = 5; // obj.value 是左值
右值:不可以取地址,也不能对其赋值。如:字面常量、表达式返回值、函数返回值
int x = 5; // 5 是右值,不能取地址
std::string("temporary"); // 这是一个右值,不能取地址
int y = x + 10; // x + 10 是右值,不能取地址
提问:左值在左,右值在右?
左值只能出现在 = 左边,右值只能出现在 = 右边吗?
虽然这种说法很符合左右值取名的定义,但是这种说法是不准确的:
int a = 10;
int b = a; // a是左值,但是在=右边
// 10 = a; // 报错,左边必须为左值
赋值符号 = 左边必须是左值(右值不行),并且是可修改的左值(const修饰的左值不行)
二、左值引用与右值引用
传统的C++语法中就有引用的语法,而C++11中新增了右值的引用语法特征,下面这些都是左值引用的情况:
int main(){// 以下的p、b、c、*p都是左值
int* p = new int(0);int b = 1;const int c = 2;// 以下几个是对上面左值的左值引用
int*& rp = p;int& rb = b;const int& rc = c;int& pvalue = *p;return 0;}
那右值引用该怎么用呢,我们怎么对字面常量 10 进行引用呢?左值引用是在类型后面加&,右值引用就是在类型后面加&&:
int main()
{double x = 1.1, y = 2.2;// 以下几个都是常见的右值10;x + y;fmin(x, y);// 以下几个都是对右值的右值引用int&& rr1 = 10;double&& rr2 = x + y;double&& rr3 = fmin(x, y);// 这里编译会报错:error C2106: “=”: 左操作数必须为左值10 = 1;x + y = 1;fmin(x, y) = 1;return 0;
}
1.提问:右值引用为左值?
左值引用是左值吗,右值引用是右值吗?
引用作为表达式的别名,它本身也是一个表达式,所以也有左右值之分,要进行区分,我们对它进行取地址,看看可不可行:
我们发现,ra作为左值引用,rb作为右值引用,它们都可以被取地址并且赋值,说明它们都是左值,这就很奇妙了,左值引用为左值并不奇怪,但是右值引用也是左值,这是为什么呢?
要了解原因,我们就得从左值右值的底层入手:
2.不能取地址≠没有地址
我们知道,字面常量(如 2)作为右值是不能取地址的,也就是&10这种做法是被禁止的,但是右值不能被取地址,就代表它没有地址吗?显然不是:
我们上面提到过,int num = 2; 这段代码被编译后会放到进程空间的正文代码区,那么系统怎么知道你要用2去初始化num呢,因为正文代码区存储了10的二进制序列以及它要放入的地址信息以及把10放入该地址的指令。
回头看这个规则:右值不能被取地址,2有地址吗?当然右,如果没有地址,系统怎么知道初始化的值是2。所以不能取地址不是因为没有地址,而是因为这个地址指向只读数据区,该地址上的数据只有在程序运行后才会被系统读取,由于数据不能被修改,所以编译器禁止取地址操作(取到地址就可以凭借地址对数据进行篡改),于是编译失败。
2的地址是禁止访问的,但是rb作为2的右值引用,却可以进行地址访问,这不应该啊,唯一合理解释就是,右值的引用与右值并不共用一块地址。
3.左右值引用的绑定
左值引用(例如int &ref = a;
)确实直接绑定到左值的地址,即原始对象的内存位置。这意味着左值引用和原始对象共享同一个地址:
但是右值引用本身并不是直接对右值地址的引用,而是编译器会分配一个新的存储地址,将右值的值拷贝到该位置。
因此可以作如下区分:
1️⃣左值引用绑定左值的 值+地址
2️⃣右值引用只绑定右值的 值,不绑定地址,额外分配一块地址
4.左右值引用的比较
左值引用:
1.左值引用只能引用左值,不能引用右值。
2.但是const左值既可以引用左值,也可以引用右值
int main()
{// 左值引用只能引用左值,不能引用右值。int a = 10;int& ra1 = a; // ra为a的别名//int& ra2 = 10; // 编译失败,因为10是右值// const左值引用既可引用左值,也可引用右值。const int& ra3 = 10;const int& ra4 = a;return 0;
}
右值引用:
1.右值引用只能引用右值,不能引用左值
2.但是右值引用可以引用move以后的左值(move将左值转化成右值)
int main()
{// 右值引用只能右值,不能引用左值。int&& r1 = 10;// error C2440: “初始化”: 无法从“int”转换为“int &&”// message : 无法将左值绑定到右值引用int a = 10;int&& r2 = a;// 右值引用可以引用move以后的左值int&& r3 = std::move(a);return 0;
}
三、右值引用的意义
右值引用到底有什么意义呢,C++11为什么要推出右值引用这个概念呢?
在右值引用出现之前,只存在左值引用,那么就说明,左值引用存在短板,需要右值引用来补齐。
1.左值引用的使用场景
作为函数参数
我们用对象作为参数传递的时候,使用左值引用可以避免对象的拷贝,在传递较大对象或包含复杂数据结构的对象时,可以显著提高效率,下面用一个自定义string类来演示,出现拷贝构造时会打印信息:
// 拷贝构造string(const string& s):_str(nullptr){cout << "string(const string& s) -- 深拷贝" << endl;string tmp(s._str);swap(tmp);}// 赋值重载string& operator=(const string& s){cout << "string& operator=(string s) -- 深拷贝" << endl;string tmp(s);swap(tmp);return *this;}
可以看到,使用左值引用避免了一次拷贝构造(深拷贝)
作为函数返回值
左值引用也可以作为函数的返回值,从而可以通过函数调用直接操作该变量,比如访问数组元素、链表节点这戏:
#include <iostream>int& getElement(int arr[], int index) {return arr[index]; // 返回数组元素的引用
}int main() {int arr[5] = {1, 2, 3, 4, 5};getElement(arr, 2) = 10; // 修改返回的元素std::cout << arr[2] << std::endl; // 输出:10return 0;
}
但是左值引用作为函数返回值的情况,有一个局限,那就是当函数返回对象是一个局部变量(出了函数作用域就不存在了),就不能使用左值引用返回,只能传值返回。这会有什么影响呢?
2.左值引用的局限
当我们用传值返回的方式返回一个局部对象,例如下面这个函数(将整形转成字符串)
my::string to_string(int value)
{bool flag = true;if (value < 0){flag = false;value = 0 - value;}my::string str; // 局部变量while (value > 0){int x = value % 10;value /= 10;str += ('0' + x);}if (flag == false){str += '-';}std::reverse(str.begin(), str.end());return str;
}int main()
{my::string str = to_string(123);
}
上面这种情况会进行几次拷贝构造?编译器说是一次,但其实是两次,这里是编译器进行优化了:
由于对象作为局部变量在函数结束时就会销毁,所以要想保留对象的内容,就需要一个临时对象来接收(即返回对象ret),此时就会调用一次拷贝构造,将局部变量的内容拷贝到返回对象中,返回对象也只是临时的,它的作用就是在外部需要接收时,再将内容拷贝构造给新的对象,所以总共是发生了两次拷贝构造:
不过现在的编译器会优化成一次拷贝构造,将局部对象直接作为函数临时对象拷贝给接收对象:
无论如何,至少都要进行一次深拷贝,面对较大对象时,会很大程度上影响性能,那么可不可以不多这一次拷贝构造呢,就是将局部对象的内容直接传给外部接收对象,左值引用不能做到的事情,右值引用可以做到。
3.右值引用和移动语义
上述拷贝构造函数的参数都是左值引用,所以我们需要重新定义拷贝构造函数,其参数列表为右值引用。
在左值引用传参的拷贝构造函数中,由于左值引用传递的对象仍然在其他地方使用,所以我们需要定义一个临时对象tmp
,开辟一块新的空间,将传入参数的数据安全地拷贝到tmp
中,然后通过swap
将tmp
的内容与this
对象进行交换,这种拷贝称为深拷贝。
而在右值引用传参的拷贝构造函数中,由于右值引用传递的对象(例如临时变量)即将销毁,我们可以直接“窃取”其资源,就不用深拷贝了,所以它叫做移动拷贝,将别人的资源转移到自己身上。
// 移动构造string(string&& s):_str(nullptr), _size(0), _capacity(0){cout << "string(string&& s) -- 移动语义" << endl;swap(s);}// 移动赋值string& operator=(string&& s){cout << "string& operator=(string&& s) -- 移动语义" << endl;swap(s);return *this;}
如此一来,大大提升了效率。
以上就是对左值引用与右值引用的介绍与个人理解,欢迎指正~
码文不易,还请多多关注支持,这是我持续创作的最大动力!
相关文章:

【C++】对左值引用右值引用的深入理解(右值引用与移动语义)
🌈 个人主页:谁在夜里看海. 🔥 个人专栏:《C系列》《Linux系列》 ⛰️ 天高地阔,欲往观之。 目录 前言:对引用的底层理解 一、左值与右值 提问:左值在左,右值在右?…...
寻找符合要求的最长子串
题目描述 给定一个字符串 s s s ,找出这样一个子串: 1)该子串中的任意一个字符最多出现2次; 2)该子串不包含指定某个字符; 请你找出满足该条件的最长子串的长度。 输入描述: 第一行为要求…...

【vim文本编辑器gcc编译器gdb调试器】
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 一、vimvim安装vim常用快捷键vim使用vimtutor zh文档 二、gcc编译器安装gcc工具编译源代码 三、gdb调试器gdb安装gdb常用指令gdb简单上手使用gdb的单步调试功能 总结…...

解决使用Golang的email库发送qq邮件报错short response,错误类型为textproto.ProtocolError
问题阐述 使用email库发送QQ邮件,采用465端口: package mainimport ("fmt""net/smtp""github.com/jordan-wright/email" )func SendEmail(sendTo string, subject string, body string) (err error) {e : email.NewEmai…...

为什么说高级工程师研究的不仅是器件的稳态,模拟器件的开关过程
高级工程师研究器件的开关过程而不仅仅是稳态,主要有以下几个原因: 1. 动态性能优化:开关过程中的特性,如上升时间、下降时间、过冲、振铃等,直接影响系统的性能。了解和优化这些动态特性对于提高系统的速度、精度和稳…...

Matlab实现白鲸优化算法(BWO)求解路径规划问题
目录 1.内容介绍 2.部分代码 3.实验结果 4.内容获取 1内容介绍 白鲸优化算法(BWO)是一种受自然界白鲸捕食行为启发的新型优化算法,它通过模拟白鲸的群体捕猎策略和社会互动来探索问题的最优解。BWO因其强大的全局搜索能力和高效的局部搜索能…...
SQLserver索引的优化和使用
在SQL Server中,索引的优化和有效使用是提高查询性能的关键。以下是一些详细的步骤和最佳实践,帮助你优化和使用索引。 1. 理解索引类型 聚集索引(Clustered Index) 定义:表中数据的物理排序顺序与索引的逻辑顺序相…...

Linux 安装nacos
1.下载版本 https://github.com/alibaba/nacos/tags 2.解压压缩包,启动 (1)将压缩包放到/usr/local目录下,解压 tar -xvf nacos-server-2.0.0-BETA.tar.gz(2)删除压缩包 rm -f nacos-server-2.0.0-BETA.tar.gz(3) 找到nacos的mysql的数据库脚本,在数…...
水仙花判断
一个三位整数,如果满足条件:个位数的立方十位数立方百位数立方这个数,那么称之为“水仙花数” 编程判断一个三位整数是不是水仙花数。 输入格式: 输入一个三位整数 输出格式: 是则输出YES,不是输出 NO (注意是大写字母…...

在VS中安装chatGPT
2、在VSCode中打开插件窗口 3、输入ChatGPT 4、这里有个ChatGPT中文版,就它了 5、安装 6、这时候侧边栏多了一个chatGPT分页图标,点击它 7、打个招呼 8、好像不行 9、看一下细节描述 10、根据要求按下按下快捷键 Ctrl Shift P 11、切换成国内模式 12、…...

算法笔记:Day-09(初始动态规划)
509. 斐波那契数 斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是: F(0) 0,F(1) 1 F(n) F(n - 1) F(n - 2),其中 …...
“探索未来医疗:生成式人工智能在医疗领域的革命性应用“
生成式人工智能(GenAI)在医疗领域的应用具有巨大的潜力和变革性,以下是一些关键的应用领域: 医学影像分析: GenAI模型通过深度学习技术,能够自动识别医学影像中的病变区域,提高诊断的准确性和速…...

数字IC后端实现Innovus 时钟树综合(Clock Tree Synthesis)典型案例
对于如下所示电路,要求以下几路做到等长,clock skew控制在50ps以内,clock tree insertion delay做到800ps! from FF/Q to FF1_1/D through the FF1 CK from FF/Q to FF2_1/D through the FF2 CK from FF/Q to FF3_1/D through the FF3 CK fr…...
Matlab应用制作入门
要在 MATLAB 中创建一个简单的应用程序,你可以使用 App Designer,这是一个用于构建交互式应用的工具。以下是一个简单的步骤,帮助你创建一个基本的 MATLAB 应用程序: 1. 打开 App Designer 在 MATLAB 命令窗口中输入 appdesigne…...
什么是声明式编程什么是函数式编程,打比方说明
在前端开发中,声明式编程和函数式编程是两种不同的编程范式,各自有其特定的理念和用法。下面详细介绍这两种编程范式,并通过比喻进行说明。 声明式编程 定义: 声明式编程是一种编程风格,强调“你想要什么”而不是“怎…...
SpringBoot+Shiro权限管理
Shiro是一个强大的Java安全框架,提供了身份验证、授权、加密、会话管理以及与Web集成等多种安全功能。以下是对Shiro权限管理的详细总结: 一、Shiro权限管理的基本概念 权限管理,一般指根据系统设置的安全规则或者安全策略,用户…...
前端面试题22 | 什么是跨域问题?怎么解决?
哈喽小伙伴们大家好!新的一周开始啦~距离2024年结束也仅有两个月了,不知道大家年初给自己制定的目标实现了多少?不管怎样,接下来的两个月都请继续加油哦!我们坚持下来了,我们就是最棒的! 今天,继续来给大家分享一道面试题 在开发中,我们经常会遇到跨域的问题,尤其是开发前后…...

HarmonyOS Next星河版笔记--界面开发(3)
属性 1.1.设计资源-svg图标 需求:界面中展示图标→可以使用的svg图标(任意放大缩小不失真、可以改变颜色) 使用方式: ①设计师提供:基于项目的图标,拷贝到项目目录使用 Image($r(app.media.ic_dianpu)) .width(40) fillColor…...

科研绘图系列:R语言组合连线图和箱线图(linechart+boxplot)
文章目录 介绍加载R包数据数据预处理画图1画图2系统信息介绍 连线图(Line Chart)是一种常用的数据可视化图表,它通过将一系列数据点用直线段连接起来来展示数据随时间或有序类别变化的趋势。以下是连线图可以表示的一些内容: 时间序列数据:展示数据随时间变化的趋势,例如…...
对象的接口与设计模式在其中的作用
对象的接口 对象的接口定义了对象的行为和如何与外界进行交互。以下是对象接口的详细解释: 成员函数(Member Functions) 定义:成员函数是定义在类中的函数,用于实现类的行为。成员函数可以通过对象来调用࿰…...

深度学习在微纳光子学中的应用
深度学习在微纳光子学中的主要应用方向 深度学习与微纳光子学的结合主要集中在以下几个方向: 逆向设计 通过神经网络快速预测微纳结构的光学响应,替代传统耗时的数值模拟方法。例如设计超表面、光子晶体等结构。 特征提取与优化 从复杂的光学数据中自…...
React hook之useRef
React useRef 详解 useRef 是 React 提供的一个 Hook,用于在函数组件中创建可变的引用对象。它在 React 开发中有多种重要用途,下面我将全面详细地介绍它的特性和用法。 基本概念 1. 创建 ref const refContainer useRef(initialValue);initialValu…...

Unity3D中Gfx.WaitForPresent优化方案
前言 在Unity中,Gfx.WaitForPresent占用CPU过高通常表示主线程在等待GPU完成渲染(即CPU被阻塞),这表明存在GPU瓶颈或垂直同步/帧率设置问题。以下是系统的优化方案: 对惹,这里有一个游戏开发交流小组&…...

大型活动交通拥堵治理的视觉算法应用
大型活动下智慧交通的视觉分析应用 一、背景与挑战 大型活动(如演唱会、马拉松赛事、高考中考等)期间,城市交通面临瞬时人流车流激增、传统摄像头模糊、交通拥堵识别滞后等问题。以演唱会为例,暖城商圈曾因观众集中离场导致周边…...
服务器硬防的应用场景都有哪些?
服务器硬防是指一种通过硬件设备层面的安全措施来防御服务器系统受到网络攻击的方式,避免服务器受到各种恶意攻击和网络威胁,那么,服务器硬防通常都会应用在哪些场景当中呢? 硬防服务器中一般会配备入侵检测系统和预防系统&#x…...
macOS多出来了:Google云端硬盘、YouTube、表格、幻灯片、Gmail、Google文档等应用
文章目录 问题现象问题原因解决办法 问题现象 macOS启动台(Launchpad)多出来了:Google云端硬盘、YouTube、表格、幻灯片、Gmail、Google文档等应用。 问题原因 很明显,都是Google家的办公全家桶。这些应用并不是通过独立安装的…...

Java-41 深入浅出 Spring - 声明式事务的支持 事务配置 XML模式 XML+注解模式
点一下关注吧!!!非常感谢!!持续更新!!! 🚀 AI篇持续更新中!(长期更新) 目前2025年06月05日更新到: AI炼丹日志-28 - Aud…...

学校时钟系统,标准考场时钟系统,AI亮相2025高考,赛思时钟系统为教育公平筑起“精准防线”
2025年#高考 将在近日拉开帷幕,#AI 监考一度冲上热搜。当AI深度融入高考,#时间同步 不再是辅助功能,而是决定AI监考系统成败的“生命线”。 AI亮相2025高考,40种异常行为0.5秒精准识别 2025年高考即将拉开帷幕,江西、…...

Selenium常用函数介绍
目录 一,元素定位 1.1 cssSeector 1.2 xpath 二,操作测试对象 三,窗口 3.1 案例 3.2 窗口切换 3.3 窗口大小 3.4 屏幕截图 3.5 关闭窗口 四,弹窗 五,等待 六,导航 七,文件上传 …...

android13 app的触摸问题定位分析流程
一、知识点 一般来说,触摸问题都是app层面出问题,我们可以在ViewRootImpl.java添加log的方式定位;如果是touchableRegion的计算问题,就会相对比较麻烦了,需要通过adb shell dumpsys input > input.log指令,且通过打印堆栈的方式,逐步定位问题,并找到修改方案。 问题…...