【C++】右值引用

目录

  • 前言:
  • 一、左值引用和右值引用
    • 1.1 什么是左值和左值引用
    • 1.2 什么是右值和右值引用
  • 二、左值引用和右值引用比较
  • 三、右值引用使用场景
    • 3.1 传值返回使用场景
    • 3.2 移动构造
    • 3.3 移动赋值
    • 3.4 STL容器接口也增加右值引用
    • 3.5 完美转发

前言:

引用是给对象取别名,本质是为了减少拷贝。以前我们学习的引用都是左值引用,右值引用是C++11新增的语法,它们的共同点都是给对象取别名。既然如此,有了左值引用,为什么还要有右值引用?右值引用具体是怎样的?以及它有哪些应用场景?接下来,会详细分析~~

一、左值引用和右值引用

1.1 什么是左值和左值引用

左值是一个表示数据的表达式,可以是变量名、解引用的指针和前置++。左值可以取地址和赋值,它出现在赋值符号的左边。如果定义的左值被const修饰,那么它就不能被赋值,但是可以取地址。

//左值
int a = 10;
const int b = 20;
int* p = new int(0);

前置++是左值是因为该运算符先进行自增,再使用,返回值还是它自己,所以是左值

左值引用就是给左值的引用,给左值取别名

//左值引用
int& c = a;
const int& d = b;
int*& pp = p;

1.2 什么是右值和右值引用

右值也是一个表示数据的表达式,可以是常量、表达式、函数返回值(不能是左值引用返回)和后置++。右值不可以被赋值和取地址,它出现在赋值符号的右边。

int x = 1, y = 2;
//右值
10;//常量
x + y;//表达式
func(x, y);//函数返回值

后置++是右值是因为该运算符先使用,再++,即它会返回当前没有自增的临时变量,然后再自己++

右值引用就是给右值的引用,给右值取别名

//右值引用
int&& r1 = 10;
int&& r2 = x + y;
int&& r3 = func(x,y);

总结:
左值是具有存储性质的对象,是要占内存空间的;右值是没有存储性质的对象,也就是临时对象
判断是左值还是右值,不能以是否可以赋值来确定,右值是不可以赋值的,左值没有const时可以,有const时不行,所以左值和右值的本质区别是能否取地址,左值可以取地址,右值不可以取地址

二、左值引用和右值引用比较

前面说过,左值引用是给左值取别名,右值引用是给右值取别名,那么有个小问题,左值引用能给右值取别名吗?右值引用又能否给左值取别名呢?答案是可以的,这里作了特殊处理:

  • const左值引用可以给右值取别名
  • 右值引用可以给move(左值)取别名
const int& a = 10;
int&& p = move(x);

move函数的作用是强制把左值转换为右值

我们知道,引用的最主要的作用是给对象取别名,减少拷贝。既然左值引用都可以给左值和右值取别名,那右值引用的出现有什么意义?

先来看下左值引用有哪些应用场景:

  • 解决函数传参的拷贝问题。函数传参时如果没有左值引用,就要进行拷贝;有左值引用,不需要拷贝。
  • 解决部分返回对象拷贝问题。返回对象出了函数作用域还在,没有问题;如果出了作用域就销毁了,就有问题。

1️⃣函数传参

string& operator=(const string& s)

2️⃣返回的对象,出了作用域还在

// 赋值重载
string& operator=(const string& s)
{string tmp(s);swap(tmp);return *this;//this指针指向的成员变量的作用域在整个类中
}

3️⃣返回的对象是局部的,出了作用域就销毁

int& Func()
{int b = 10;return b;
}

第一个和第二个没问题,第三个就有问题,返回对象是一个局部对象,出了作用域就销毁,用其他变量接收会出问题。

从这里可以发现,函数返回一个对象时用左值引用在某些场景是不适合的,但把左值引用去掉,只能传值返回,要拷贝。对上面的例子,返回的是一个int类型的对象,没有多大的消耗;但是如果返回的对象消耗很大,就影响效率,比如:

yss::string to_string(int value)
{bool flag = true;if (value < 0){flag = false;value = 0 - value;}yss::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;//出了函数作用域就会销毁
}

既然左值引用返回不行,传值返回有拷贝存在,那换成右值引用返回呢?其实也是不行的。
在这里插入图片描述
因为就算把要返回的对象转换为右值,还是避免不了返回对象出了作用域就销毁的情况。

三、右值引用使用场景

3.1 传值返回使用场景

怎么解决前面的问题呢?先来看看传值返回的场景:
在这里插入图片描述
在编译器没有作优化的情况下,要返回的对象是局部的,出了作用域就会销毁,所以拷贝构造给临时对象,临时对象是右值,临时对象再拷贝构造给ret1。整个过程拷贝构造了两次,拷贝了就算了,第一次拷贝构造后,str销毁了;第二次拷贝构造后,临时对象销毁了。也就是说,产生的临时空间,用完就将被销毁,这样是不是太浪费资源了。所以编译器一般都会作优化处理,尽可能的减少拷贝次数。先看下运行结果:
在这里插入图片描述
只调用了一次拷贝构造:
在这里插入图片描述

3.2 移动构造

有了编译器的优化,拷贝的次数减少,但还是不够。因此,右值引用的就有它的用武之地了。先说明一下,在前面的例子中,用右值引用作返回值是不行,因为没有解决局部对象出作用域就销毁的根本问题;也就是说,右值引用并不是像左值引用那样,你用了,就直接起作用,右值引用是间接起作用的。

右值引用是怎么间接起作用的呢?对比以下两个函数:

//函数1
void func(const int& x)
{cout << "void func(const int& x)" << endl;
}
//函数2
void func(int&& x)
{cout << "void func(int&& x)" << endl;
}int main()
{int x = 2;func(x);func(10);return 0;
}

函数2是函数1的重载,函数1的参数是左值引用,函数2的是右值引用,先注释掉函数2,运行一下:

在这里插入图片描述
第一次调用传入参数X,第二次调用传入参数10都可以调用函数1,这里其实也顺便验证了左值引用既可以引用左值(参数x),也可以引用右值(常数10,特殊处理的要记得带const)。

取消注释,函数1和函数2都在的情况下如何:
在这里插入图片描述
传入参数x调用函数1,参数为10调用函数2,说明调用哪个函数是根据传的参数是左值还是右值决定的,也就是哪个更合适用哪个。

在上面例子的基础上,可以对拷贝构造进行重载,变成移动构造,移动构造的作用:窃取别人的资源来构造自己。下面是拷贝构造和移动构造:

// 拷贝构造
string(const string& s)
{cout << "string(const string& s) -- 深拷贝" << endl;string tmp(s._str);//调用构造函数swap(tmp);
}// 移动构造
string(string&& s)
{cout << "string(string&& s) -- 移动构造" << endl;swap(s);
}
/
yss::string ret1 = yss::to_string(1234);

在拷贝构造函数和移动构造函数都在的情况下运行,只有移动构造,也就是说没有拷贝了,这得益于编译器的优化。
在这里插入图片描述

在编译器没有优化的情况下:
在这里插入图片描述

编译器有优化的情况下:
在这里插入图片描述

对比下拷贝构造和移动构造:

  • 根据函数调用匹配原则,如果传入的参数是左值,调用的是拷贝构造;如果传入的参数是右值,调用的是移动构造。
  • 拷贝构造(深拷贝)是比较浪费资源的,产生的临时对象tmp用完就销毁了;移动构造只需将被拷贝对象的资源占为己有,不需要深拷贝,提高了效率。
  • 如果没有移动构造,不管是左值还是右值都会调用拷贝构造,也就是前面例子中返回对象有两次拷贝构造的情况(假设没有优化)

是不是所有的类都要有移动构造呢?
首先要清楚的是,移动构造是为了减少拷贝。也不是所有的拷贝都需要移动构造来解决,如果是要开空间的(深拷贝),比如string类,list等就要移动构造减少拷贝,否则拷贝的消耗很大。如果是不需要开空间的(浅拷贝),比如日期类,成员变量都是int类型,像这样的内置类型直接拷贝即可。

总结:

  • 浅拷贝的类不需要移动构造
  • 深拷贝的类需要移动构造

3.3 移动赋值

右值引用不仅可以用在移动构造,还可以用在移动赋值。如果一个对象已经存在,调用的函数返回值赋值给这个对象,就会调用移动赋值。

比如:

yss::string ret1;
ret1 = yss::to_string(1234);

拷贝赋值(赋值重载)和移动赋值:

// 赋值重载
string& operator=(const string& s)
{cout << "string& operator=(string s) -- 深拷贝" << endl;string tmp(s);//调用拷贝构造swap(tmp);return *this;
}// 移动赋值
string& operator=(string&& s)
{cout << "string& operator=(string&& s) -- 移动赋值" << endl; swap(s);return *this;
}

运行一下:
在这里插入图片描述

接下来,作几组对比:
1️⃣没有移动构造和移动赋值
在这里插入图片描述
在这里插入图片描述

函数返回值是拷贝构造出来的临时对象,再赋值给已经存在的对象ret1,赋值的过程中调用赋值构造,赋值构造里又有拷贝构造,总共两次拷贝。

2️⃣有移动构造,没有移动赋值
在这里插入图片描述
在这里插入图片描述
返回对象时是移动构造,没有拷贝,但是赋值时要调用拷贝构造

3️⃣有移动构造,有移动赋值
在这里插入图片描述
在这里插入图片描述
返回对象时没有拷贝,str是出了作用域就销毁,直接给返回值,返回值也是临时对象,直接给ret1,不需要拷贝,减少了资源浪费,效率提高。

拷贝赋值与移动赋值对比:

  • 如果没有移动赋值,那么无论是左值还是右值都会调用拷贝复制,这点与拷贝构造与移动构造相同
  • 根据函数调用匹配原则,参数是左值调用拷贝赋值,参数是右值调用移动赋值
  • 拷贝赋值会先调用拷贝构造,再进行资源交换,交换后那个临时的对象用完就销毁了,整个过程比较浪费资源。移动赋值直接将自己的资源与临时对象的资源进行交换,交换后自己原来的资源只需交给临时对象处理(销毁)

注:有可能要赋值的对象不是临时对象,即不是右值,有可能是左值,那么情况就会有变化(对应函数调用匹配原则),下面来看看是左值的:

yss::string ret1;
yss::string ret2;//左值
ret1 = ret2;

运行:
在这里插入图片描述

3.4 STL容器接口也增加右值引用

有了右值引用,STL容器接口也作出了调整。以list的构造为例:
在这里插入图片描述
不仅是构造函数,在其他接口也有增加与右值引用相关的功能。通过STL中list的尾插函数来看:

list<yss::string> lt;
yss::string s1("1111");lt.push_back(s1);//有名对象
cout << "-----------------" << endl;
lt.push_back(yss::string("2222"));//匿名对象
cout << "-----------------" << endl;
lt.push_back("3333");//隐式类型转换

在这里插入图片描述
有名对象是左值,调用拷贝构造;匿名对象和隐式类型转换(构造+拷贝构造-》构造)是右值,调用移动构造。当然,在调用对应的构造函数前,尾插函数传参的过程需要先看下:

在这里插入图片描述

注:有名对象——左值可以通过move转换为右值,但是不要轻易使用,因为一旦使用这个左值的资源将会被拿走

上面的list是C++标准库中的list,用我们之前模拟实现的list试下,看有没有同样的效果。
在这里插入图片描述

第一个有两次拷贝构造,是因为定义空的链表时也有拷贝构造,第一个下面的深拷贝才是按照图示走的,所以第一个上面的深拷贝暂时先忽略掉

发现全是深拷贝,因为我们没有重载拷贝构造函数的传参为右值引用,重载后再运行看看:

ListNode(const T& x = T()):_prev(nullptr), _next(nullptr), _val(x)
{}ListNode(T&& x):_prev(nullptr), _next(nullptr), _val(x)
{}
//
//尾插
void push_back(const T& x)
{insert(end(), x);
}
void push_back(T&& x)
{insert(end(), x);
}
///
//pos位置插入
iterator insert(iterator pos, const T& x)
{Node* newnode = new Node(x);//创建新节点//......
}
iterator insert(iterator pos, T&& x)
{Node* newnode = new Node(x);//创建新节点//......
}

在这里插入图片描述
为什么还全是深拷贝呢?先来看一小段代码:
在这里插入图片描述
右值引用接收右值常量10,右值引用r可以自增++,也就是说,右值引用r的属性是左值。根据这点,所以前面的代码用右值引用参数接收后,它的属性变成了左值,左值再调用到下一个函数,接收的是左值引用。这里需要修改下代码,传参时move下,让它的参数(进入右值引用的)变成左值后再重新变成右值

ListNode(T&& x):_prev(nullptr), _next(nullptr), _val(move(x))
{}
///
void push_back(T&& x)
{insert(end(), move(x));
}
///
iterator insert(iterator pos, T&& x)
{Node* newnode = new Node(move(x));//创建新节点//......
}

运行一下:正是我们想要的结果。
在这里插入图片描述

那为什么右值引用后它的属性要变成左值呢?
在这里插入图片描述
因为只有右值引用的属性是左值可以被改变,资源才可以转移。

3.5 完美转发

模板中的万能引用——&&
作用:可以接收左值,也可以接收右值

template<class T>
void PerfectForward(T&& t)
{cout << "void PerfectForward(T&& t)" << endl;
}int main()
{PerfectForward(10);  // 右值int a = 1;PerfectForward(a);// 左值PerfectForward(move(a)); // 右值return 0;
}

在这里插入图片描述

注意:万能引用虽然和右值引用都是两个取地址符,但是要有所区分。右值引用接收右值,或者是move后的左值;万能引用左、右值都能接收,包括const左值和const右值

const int b = 8;
PerfectForward(b);// const 左值
PerfectForward(std::move(b)); // const 右值

在这里插入图片描述

这样来看好像万能引用很不错,但其实还是有些局限:

void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }template<typename T>
void PerfectForward(T&& t)
{Fun(t);
}
int main()
{PerfectForward(10); //右值int a;PerfectForward(a); //左值PerfectForward(std::move(a)); //右值const int b = 8;PerfectForward(b); //const左值PerfectForward(std::move(b)); //const右值return 0;
}

以上代码中我们的思路是:传入右值,在PerfectForward函数中调用的函数打印右值引用;传入左值,在PerfectForward函数中调用的函数打印左值引用;传入const右值,在PerfectForward函数中调用的函数打印const右值引用;传入const左值,在PerfectForward函数中调用的函数打印const左值引用。

运行结果:
在这里插入图片描述

发现都是左值,为什么?因为万能引用只是接收了而已,对后面该引用是左值引用还是右值引用就不归它管了。前面提过,左值经过左值引用后,还是左值;右值经过右值引用后,属性改变为左值。所以这段代码里无论左值进来还是右值进来最后都是调用左值引用的函数(const对应const的)。

既然这样,那么在调用Fun函数时把参数move下行不行呢?

Fun(move(t));

在这里插入图片描述
全都是右值引用了……

为了解决该问题,有一新语法:完美转发——std::forward
作用:在传参的过程中保留对象原生类型属性

Fun(std::forward<T>(t));

在这里插入图片描述

对比move和forward

  • move就是简单粗暴的把左值属性变成右值属性
  • forward是保持原来的属性。如果本身是左值,就不变;如果本身是右值,右值引用后属性会变成左值,但是这里面的过程相当于被move了,又变成了右值

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

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

相关文章

HarmonyOS 应用开发之模型切换

本文介绍如何将一个FA模型开发的声明式范式应用切换到Stage模型&#xff0c;您需要完成如下动作&#xff1a; 工程切换&#xff1a;新建一个Stage模型的应用工程。 配置文件切换&#xff1a;config.json切换为app.json5和module.json5。 组件切换&#xff1a;PageAbility/Serv…

不同的batch_size对精度和损失的影响研究

1 问题 不同的batch_size对训练集和验证集的精度和损失的影响有多大&#xff1f; 2 方法 通过设置不同batch_size算出不同batch_size对应的训练集精度、训练集损失和验证集的精度和损失&#xff0c;通过数据可视化将精度和损失展示出来&#xff0c;比较出不同batch_size对他们的…

CTK插件框架学习-插件注册调用(03)

CTK插件框架学习-新建插件(02)https://mp.csdn.net/mp_blog/creation/editor/136923735 一、CTK插件组成 接口类&#xff1a;对外暴露的接口&#xff0c;供其他插件调用实现类&#xff1a;实现接口内的方法激活类&#xff1a;负责将插件注册到CTK框架中 二、接口、插件、服务…

文生视频大模型Sora的复现经验

大家好&#xff0c;我是herosunly。985院校硕士毕业&#xff0c;现担任算法研究员一职&#xff0c;热衷于机器学习算法研究与应用。曾获得阿里云天池比赛第一名&#xff0c;CCF比赛第二名&#xff0c;科大讯飞比赛第三名。拥有多项发明专利。对机器学习和深度学习拥有自己独到的…

BFS专题

1、BFS解决FloodFill算法 1、1图像渲染 733. 图像渲染 - 力扣(LeetCode) class Solution {typedef pair<int,int> PII;int dx[4] = {0,0,1,-1};int dy[4] = {1,-1,0,0}; public:vector<vector<int>> floodFill(vector<vector<int>>& i…

RIP环境下的MGRE 综合实验

实验题目及要求&#xff1a; 1.R5为ISP&#xff0c;只能进行IP地址配置&#xff0c;其所有地址均配为公有IP地址 2.R1和R5间使用PPP的PAP认证&#xff0c;R5为主认证方; R2于R5之间使用PPP的chap认证&#xff0c;R5为主认证方&#xff1b; R3于R5之间使用HDLC封装。 3.R1/…

【C++】为什么能实现函数重载

从C语言一路学到C的途中&#xff0c;C语言C语言相比&#xff0c;多了个函数重载&#xff0c;那么函数重载是如何实现的呢&#xff0c;为什么C语言无法支持&#xff0c;在本篇博客中&#xff0c;将会讲解C为何能实现函数重载。 一.编译过程 C能实现函数重载&#xff0c;而C语言不…

QT 二维坐标系显示坐标点及点与点的连线-通过定时器自动添加随机数据点

QT 二维坐标系显示坐标点及点与点的连线-通过定时器自动添加随机数据点 功能介绍头文件C文件运行过程 功能介绍 上面的代码实现了一个简单的 Qt 应用程序&#xff0c;其功能包括&#xff1a; 创建一个 MainWindow 类&#xff0c;继承自 QMainWindow&#xff0c;作为应用程序的…

2024软件设计师备考讲义——UML(统一建模语言)

UML的概念 用例图的概念 包含 <<include>>扩展<<exted>>泛化 用例图&#xff08;也可称用例建模&#xff09;描述的是外部执行者&#xff08;Actor&#xff09;所理解的系统功能。用例图用于需求分析阶段&#xff0c;它的建立是系统开发者和用户反复…

Pyppeteer中Chromium安装步骤

1、下载压缩文件 在官网下载chrome-win.zip文件 2、终端下载pyppeteer 首先在Pycharm终端运行pip install pyppeteer 3、查找文件默认路径 在运行以下代码&#xff0c;找到可执行文件默认路径 import pyppeteer.chromium_downloader print(默认版本是&#xff1a;{}.forma…

牛角工具箱源码 轻松打造个性化在线工具箱

&#x1f389; Whats this&#xff1f; 这是一款在线工具箱程序&#xff0c;您可以通过安装扩展增强她的功能 通过插件模板的功能&#xff0c;您也可以把她当做网页导航来使用~ 觉得该项目不错的可以给个Star~ &#x1f63a; 演示地址 https://tool.aoaostar.com &#x1f…

TCP网络协议栈和Posix网络部分API总结

文章目录 Posix网络部分API综述TCP协议栈通信过程TCP三次握手和四次挥手&#xff08;看下图&#xff09;三次握手常见问题&#xff1f;为什么是三次握手而不是两次&#xff1f;三次握手和哪些函数有关&#xff1f;TCP的生命周期是从什么时候开始的&#xff1f; 四次挥手通信状态…

HarmonyOS实战开发-如何实现一个自定义抽奖圆形转盘

介绍 本篇Codelab是基于画布组件、显式动画&#xff0c;实现的一个自定义抽奖圆形转盘。包含如下功能&#xff1a; 通过画布组件Canvas&#xff0c;画出抽奖圆形转盘。通过显式动画启动抽奖功能。通过自定义弹窗弹出抽中的奖品。 相关概念 Stack组件&#xff1a;堆叠容器&am…

从0开始搭建基于VUE的前端项目(一) 项目创建和配置

准备与版本 安装nodejs(v20.11.1)安装vue脚手架(@vue/cli 5.0.8) ,参考(https://cli.vuejs.org/zh/)vue版本(2.7.16),vue2的最后一个版本vue.config.js的配置详解(https://cli.vuejs.org/zh/config/)element-ui(2.15.14)(https://element.eleme.io/)vuex(3.6.2) (https://…

K8S命令行可视化实验

以下为K8s命令行可视化工具的实验内容&#xff0c;相比于直接使用命令行&#xff0c;可视化工具可能更直观、更易于操作。 Lens Lens是用于监控和调试的K8S IDE。可以在Windows、Linux以及Mac桌面上完美运行。在 Kubernetes 上&#xff1a; 托管地址&#xff1a;github/lensa…

机器人运动控制

一、基础 1.1 矢量速度和旋转速度 矢量速度用来控制运动方向&#xff0c;任何一个方向都可以看成x、y、z三轴方向的合。单位规定是m/s。 旋转速度用来控制旋转方向&#xff0c;可以看成x、y、z三轴方向旋转的合。单位规定是pi/s。 速度消息包&#xff0c;可以在ROS Index上搜…

助力福建新型职业农民培育 北方天途推进无人机植保应用培训

为加强新型职业农民的职业培育&#xff0c;扩展新型农民的知识范围和专业技术水平&#xff0c;推进农业供给侧结构性改革。日前&#xff0c;在农业部门的大力支持下&#xff0c;北方天途航空和宁德天禾科技服务携手为福建省农民朋友开展了植保无人机驾驶员的应用培训。福建省农…

网页布局案例 浮动

这里主要讲浮动 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>Title</title><style>*{padding: 0;margin: 0;}.header{height: 40px;background-color: #333;}.nav{width: 1226px;heig…

深入理解数据结构(2):顺序表和链表详解

文章主题&#xff1a;顺序表和链表详解&#x1f331;所属专栏&#xff1a;深入理解数据结构&#x1f4d8;作者简介&#xff1a;更新有关深入理解数据结构知识的博主一枚&#xff0c;记录分享自己对数据结构的深入解读。&#x1f604;个人主页&#xff1a;[₽]的个人主页&#x…

机器学习——降维算法-奇异值分解(SVD)

机器学习——降维算法-奇异值分解&#xff08;SVD&#xff09; 在机器学习中&#xff0c;降维是一种常见的数据预处理技术&#xff0c;用于减少数据集中特征的数量&#xff0c;同时保留数据集的主要信息。奇异值分解&#xff08;Singular Value Decomposition&#xff0c;简称…