【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) 定义:成员函数是定义在类中的函数,用于实现类的行为。成员函数可以通过对象来调用࿰…...

突破不可导策略的训练难题:零阶优化与强化学习的深度嵌合
强化学习(Reinforcement Learning, RL)是工业领域智能控制的重要方法。它的基本原理是将最优控制问题建模为马尔可夫决策过程,然后使用强化学习的Actor-Critic机制(中文译作“知行互动”机制),逐步迭代求解…...
DeepSeek 赋能智慧能源:微电网优化调度的智能革新路径
目录 一、智慧能源微电网优化调度概述1.1 智慧能源微电网概念1.2 优化调度的重要性1.3 目前面临的挑战 二、DeepSeek 技术探秘2.1 DeepSeek 技术原理2.2 DeepSeek 独特优势2.3 DeepSeek 在 AI 领域地位 三、DeepSeek 在微电网优化调度中的应用剖析3.1 数据处理与分析3.2 预测与…...
React Native 导航系统实战(React Navigation)
导航系统实战(React Navigation) React Navigation 是 React Native 应用中最常用的导航库之一,它提供了多种导航模式,如堆栈导航(Stack Navigator)、标签导航(Tab Navigator)和抽屉…...
Oracle查询表空间大小
1 查询数据库中所有的表空间以及表空间所占空间的大小 SELECTtablespace_name,sum( bytes ) / 1024 / 1024 FROMdba_data_files GROUP BYtablespace_name; 2 Oracle查询表空间大小及每个表所占空间的大小 SELECTtablespace_name,file_id,file_name,round( bytes / ( 1024 …...
【Java学习笔记】Arrays类
Arrays 类 1. 导入包:import java.util.Arrays 2. 常用方法一览表 方法描述Arrays.toString()返回数组的字符串形式Arrays.sort()排序(自然排序和定制排序)Arrays.binarySearch()通过二分搜索法进行查找(前提:数组是…...

Swift 协议扩展精进之路:解决 CoreData 托管实体子类的类型不匹配问题(下)
概述 在 Swift 开发语言中,各位秃头小码农们可以充分利用语法本身所带来的便利去劈荆斩棘。我们还可以恣意利用泛型、协议关联类型和协议扩展来进一步简化和优化我们复杂的代码需求。 不过,在涉及到多个子类派生于基类进行多态模拟的场景下,…...

【配置 YOLOX 用于按目录分类的图片数据集】
现在的图标点选越来越多,如何一步解决,采用 YOLOX 目标检测模式则可以轻松解决 要在 YOLOX 中使用按目录分类的图片数据集(每个目录代表一个类别,目录下是该类别的所有图片),你需要进行以下配置步骤&#x…...

OPenCV CUDA模块图像处理-----对图像执行 均值漂移滤波(Mean Shift Filtering)函数meanShiftFiltering()
操作系统:ubuntu22.04 OpenCV版本:OpenCV4.9 IDE:Visual Studio Code 编程语言:C11 算法描述 在 GPU 上对图像执行 均值漂移滤波(Mean Shift Filtering),用于图像分割或平滑处理。 该函数将输入图像中的…...
Java多线程实现之Thread类深度解析
Java多线程实现之Thread类深度解析 一、多线程基础概念1.1 什么是线程1.2 多线程的优势1.3 Java多线程模型 二、Thread类的基本结构与构造函数2.1 Thread类的继承关系2.2 构造函数 三、创建和启动线程3.1 继承Thread类创建线程3.2 实现Runnable接口创建线程 四、Thread类的核心…...

无人机侦测与反制技术的进展与应用
国家电网无人机侦测与反制技术的进展与应用 引言 随着无人机(无人驾驶飞行器,UAV)技术的快速发展,其在商业、娱乐和军事领域的广泛应用带来了新的安全挑战。特别是对于关键基础设施如电力系统,无人机的“黑飞”&…...