【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中,然后通过swaptmp的内容与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;}

如此一来,大大提升了效率。


以上就是对左值引用与右值引用的介绍与个人理解,欢迎指正~

码文不易,还请多多关注支持,这是我持续创作的最大动力!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/59097.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

解决 ClickHouse 高可用集群中 VRID 冲突问题:基于 chproxy 和 keepalived 的实践分析

Part1背景描述 近期&#xff0c;我们部署了两套 ClickHouse 生产集群&#xff0c;分别位于同城的两个数据中心。这两套集群的数据保持一致&#xff0c;以便在一个数据中心发生故障时&#xff0c;能够迅速切换应用至另一个数据中心的 ClickHouse 实例&#xff0c;确保服务连续性…

B2C电商平台如何提升转化率 小程序商城如何做好运营

在竞争激烈的电商市场中&#xff0c;提升转化率是每个B2C电商平台的重要目标。转化率直接影响销售业绩和盈利能力&#xff0c;因此&#xff0c;了解如何优化用户体验、增强客户信任和提高购买动机是至关重要的。商淘云分享一些有效的策略&#xff0c;帮助B2C电商平台提升转化率…

RK3568平台开发系列讲解(字符设备驱动篇)Linux设备分类

🚀返回专栏总目录 文章目录 一、字符设备(是以字节为单位进行输入输出)二、块设备:块设备是以块为单位进行输入输出三、网络设备沉淀、分享、成长,让自己和他人都能有所收获!😄 一、字符设备(是以字节为单位进行输入输出) 串口、鼠标 字符设备没有固定的大小,也没…

STM32Fxx读写eeprom(AT24C16)

一.I2C 协议简介 I2C 通讯协议 (Inter &#xff0d; Integrated Circuit) 是由 Phiilps 公司开发的&#xff0c;由于它引脚少&#xff0c;硬件实现简单&#xff0c;可扩展性强&#xff0c;不需要 USART、CAN 等通讯协议的外部收发设备&#xff0c;现在被广泛地使用在系统内多个…

idea使用Translation插件实现翻译

1.打开idea&#xff0c;settings&#xff0c;选择plugins&#xff0c;搜索插件Translation&#xff0c;安装 2.选择翻译引擎 3.配置引擎&#xff0c;以有道词典为例 3.1 获取应用ID&#xff0c;应用秘钥 3.1.1 创建应用 点击进入有道智云控制台 3.1.2 复制ID和秘钥 3.2 idea设…

【论文精读】LPT: Long-tailed prompt tuning for image classification

&#x1f308; 个人主页&#xff1a;十二月的猫-CSDN博客 &#x1f525; 系列专栏&#xff1a; &#x1f3c0;论文精读_十二月的猫的博客-CSDN博客 &#x1f4aa;&#x1f3fb; 十二月的寒冬阻挡不了春天的脚步&#xff0c;十二点的黑夜遮蔽不住黎明的曙光 目录 1. 摘要 2. …

【SQL Server】解决因使用 varchar 类型存储 Unicode 字符串导致的中文显示乱码问题

问题描述 导入 SQL 到 SQL Server 数据库后&#xff0c;存在部分列的中文显示异常的问题。 原因分析 观察发现显示异常的字段的数据类型是 varchar&#xff0c;而显示正常的字段的数据类型是 nvarchar。 而且&#xff0c;SQL 文件中所有字符串前面都带有 N 的前缀。 在 SQL 中…

dify实战案例分享-基于多模态模型的发票识别

1 什么是dify Dify是一个开源的大语言模型&#xff08;LLM&#xff09;应用开发平台&#xff0c;旨在简化和加速生成式AI应用的创建和部署。它结合了后端即服务&#xff08;Backend as Service, BaaS&#xff09;和LLMOps的理念&#xff0c;使开发者能够快速搭建生产级的AI应用…

电机控制储备知识 一 电机驱动本质分析以及磁相关的使用场景

一&#xff1a;电机旋转的原因 1.电机基本认识 &#xff08;1&#xff09;电机是一种动力装置&#xff0c;能够将电能转换为动能 电机拥有体积小 、动力足&#xff0c;控制精细灵活的特点 完整的电机系统&#xff1a;电机&#xff08;减速器 传感器&#xff09; 电机驱动器&a…

ubuntu交叉编译dbus库给arm平台使用

1.下载dbus库源码 https://www.freedesktop.org/wiki/Software/dbus 克隆源码: https://gitlab.freedesktop.org/dbus/dbus/-/tree/dbus-1.12?ref_type=heads 下载1.12.20版本: 指定pkgconfig环境变量: export PKG_CONFIG_PATH=$PKG_CONFIG_PATH:$PWD/../expat-2.3.…

推荐一款音乐神器:想听就听,想下就下~

在这个音乐充斥生活的时代&#xff0c;你是否也曾想过&#xff0c;有一款软件可以随时随地畅听你喜欢的音乐&#xff1f;今天&#xff0c;我要向你推荐一款令人心动的音乐神器——MusicFree&#xff0c;让你真正体验“想听就听&#xff0c;想下就下”的乐趣&#xff01; 那么&a…

aws(学习笔记第十课) 对AWS的EBS如何备份(snapshot)以及使用snapshot恢复数据,AWS实例存储

aws(学习笔记第十课) 对AWS的EBS如何备份&#xff08;snapshot&#xff09;以及使用snapshot&#xff0c;AWS实例存储 学习内容&#xff1a; 对AWS的EBS如何备份AWS实例存储EBS和实例存储的不足 1. 对AWS的EBS如何备份&#xff08;snapshot&#xff09;以及使用snapshot恢复数…

Unity照片墙效果

Unity照片墙效果&#xff0c;如下效果展示 。 工程源码

qt QMenu详解

1、概述 QMenu是Qt框架中的一个类&#xff0c;用于创建和管理菜单。它提供了丰富的接口来添加菜单项&#xff08;通常是QAction对象&#xff09;、子菜单以及分隔符。QMenu可以嵌入到菜单栏&#xff08;QMenuBar&#xff09;中&#xff0c;也可以作为弹出菜单&#xff08;通过…

倍增 st表 RMQ问题

本章我们来谈谈&#xff0c;倍增 && st表 && RMQ问题。 倍增 倍增即成倍增长。是指我们在进行递推时&#xff0c;如果状态空间很大&#xff0c;线性递推无法满足时空要求&#xff0c;此时可以考虑成倍增长的方式&#xff0c;只递推状态空间在2的整数次幂位置上…

51单片机应用开发(进阶)---定时器应用(电子秒表)

实现目标 1、巩固定时器的配置流程&#xff1b; 2、掌握按键、数码管与定时器配合使用&#xff1b; 3、秒表具体实现&#xff1a;&#xff08;1&#xff09;按K1开始计时&#xff0c;再按K1暂停计时.......&#xff1b;&#xff08;2&#xff09;按K2计时清零&#xff1b;&a…

吴恩达深度学习笔记(11)13.

人脸检校&#xff1a;输入某人的照片以及姓名或ID&#xff0c;判断是不是他所说的那个人 人脸识别&#xff1a; 单样本学习&#xff1a; 大多数人脸识别的问题需要在样本中只有一张照片的情况下认出一个人。 相似性函数&#xff1a;利用神经网络训练一个函数&#xff0c;可…

链表详解(三)

目录 链表功能实现链表的查找SLNode* SLFind(SLNode* phead, SLNDataType x)代码 链表任意位置前插入void SLInsert(SLNode**pphead&#xff0c;SLNode* pos, SLNDataType x)代码 链表任意位置前删除void SLErase(SLNode**pphead&#xff0c;SLNode* pos)代码 链表任意位置后插…

《C#语法一篇通》,20万字,48小时阅读,持续完善中。。。

本文摘录了C#语法的主要内容&#xff0c;接近20万字。 所有鸡汤的味道都等于马尿&#xff01; 如果你相信任何所谓的鸡汤文章&#xff0c;智商堪忧。 计算机语言没有”好不好“之说&#xff0c;骗子才会告诉你哪个语言好&#xff0c;学好任何一本基础语言&#xff08;C&#…

HarmonyOS开发 - 餐饮APP中多门店多窗口打开实例补充

specified启动模式为指定实例模式&#xff0c;有一些特殊场景&#xff0c;例如多门店应用中每次打开一个门店都希望能新建一个门店实例&#xff0c;而重复打开同一个门店都是同一门店实例。 此篇为餐饮APP中多门店实例的补充内容&#xff0c;以解决同一门店多次点击重复创建新窗…