【C++】右值引用

这篇具有很好参考价值的文章主要介绍了【C++】右值引用。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前言:

引用是给对象取别名,本质是为了减少拷贝。以前我们学习的引用都是左值引用,右值引用是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;//出了函数作用域就会销毁
}

既然左值引用返回不行,传值返回有拷贝存在,那换成右值引用返回呢?其实也是不行的。
【C++】右值引用,C++,c++,开发语言,c语言
因为就算把要返回的对象转换为右值,还是避免不了返回对象出了作用域就销毁的情况。

三、右值引用使用场景

3.1 传值返回使用场景

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

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,运行一下:

【C++】右值引用,C++,c++,开发语言,c语言
第一次调用传入参数X,第二次调用传入参数10都可以调用函数1,这里其实也顺便验证了左值引用既可以引用左值(参数x),也可以引用右值(常数10,特殊处理的要记得带const)。

取消注释,函数1和函数2都在的情况下如何:
【C++】右值引用,C++,c++,开发语言,c语言
传入参数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);

在拷贝构造函数和移动构造函数都在的情况下运行,只有移动构造,也就是说没有拷贝了,这得益于编译器的优化。
【C++】右值引用,C++,c++,开发语言,c语言

在编译器没有优化的情况下:
【C++】右值引用,C++,c++,开发语言,c语言

编译器有优化的情况下:
【C++】右值引用,C++,c++,开发语言,c语言

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

  • 根据函数调用匹配原则,如果传入的参数是左值,调用的是拷贝构造;如果传入的参数是右值,调用的是移动构造。
  • 拷贝构造(深拷贝)是比较浪费资源的,产生的临时对象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;
}

运行一下:
【C++】右值引用,C++,c++,开发语言,c语言

接下来,作几组对比:
1️⃣没有移动构造和移动赋值
【C++】右值引用,C++,c++,开发语言,c语言
【C++】右值引用,C++,c++,开发语言,c语言

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

2️⃣有移动构造,没有移动赋值
【C++】右值引用,C++,c++,开发语言,c语言
【C++】右值引用,C++,c++,开发语言,c语言
返回对象时是移动构造,没有拷贝,但是赋值时要调用拷贝构造

3️⃣有移动构造,有移动赋值
【C++】右值引用,C++,c++,开发语言,c语言
【C++】右值引用,C++,c++,开发语言,c语言
返回对象时没有拷贝,str是出了作用域就销毁,直接给返回值,返回值也是临时对象,直接给ret1,不需要拷贝,减少了资源浪费,效率提高。

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

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

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

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

运行:
【C++】右值引用,C++,c++,开发语言,c语言

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

有了右值引用,STL容器接口也作出了调整。以list的构造为例:
【C++】右值引用,C++,c++,开发语言,c语言
不仅是构造函数,在其他接口也有增加与右值引用相关的功能。通过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");//隐式类型转换

【C++】右值引用,C++,c++,开发语言,c语言
有名对象是左值,调用拷贝构造;匿名对象和隐式类型转换(构造+拷贝构造-》构造)是右值,调用移动构造。当然,在调用对应的构造函数前,尾插函数传参的过程需要先看下:

【C++】右值引用,C++,c++,开发语言,c语言

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

上面的list是C++标准库中的list,用我们之前模拟实现的list试下,看有没有同样的效果。
【C++】右值引用,C++,c++,开发语言,c语言

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

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

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);//创建新节点
	//......
}

【C++】右值引用,C++,c++,开发语言,c语言
为什么还全是深拷贝呢?先来看一小段代码:
【C++】右值引用,C++,c++,开发语言,c语言
右值引用接收右值常量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));//创建新节点
	//......
}

运行一下:正是我们想要的结果。
【C++】右值引用,C++,c++,开发语言,c语言

那为什么右值引用后它的属性要变成左值呢?
【C++】右值引用,C++,c++,开发语言,c语言
因为只有右值引用的属性是左值可以被改变,资源才可以转移。

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

【C++】右值引用,C++,c++,开发语言,c语言

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

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

【C++】右值引用,C++,c++,开发语言,c语言

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

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左值引用。

运行结果:
【C++】右值引用,C++,c++,开发语言,c语言

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

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

Fun(move(t));

【C++】右值引用,C++,c++,开发语言,c语言
全都是右值引用了……

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

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

【C++】右值引用,C++,c++,开发语言,c语言

对比move和forward文章来源地址https://www.toymoban.com/news/detail-847554.html

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

到了这里,关于【C++】右值引用的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 初识C++之左值引用与右值引用

    目录 一、左值引用与右值引用 1. 左值和右值的概念 1.1 左值 1.2 右值  1.3 左值与右值的区分 2. 左值引用与右值引用 2.1 左值引用与右值引用的使用方法 2.2 左值引用的可引用范围 2.3 右值引用的可引用范围 3. 右值引用的作用 3.1 减少传值返回的拷贝 3.2 插入时的右值引用 4

    2023年04月26日
    浏览(40)
  • 【C++】右值引用(极详细版)

    在讲右值引用之前,我们要了解什么是右值?那提到右值,就会想到左值,那左值又是什么呢? 我们接下来一起学习!   目录 1.左值引用和右值引用 1.左值和右值的概念 2.左值引用和右值引用的概念 2.左值引用和右值引用引出 3.右值引用的价值 1.补齐左值引用的短板——函

    2024年02月11日
    浏览(39)
  • 【C++】C++11右值引用

    👀 樊梓慕: 个人主页  🎥 个人专栏: 《C语言》 《数据结构》 《蓝桥杯试题》 《LeetCode刷题笔记》 《实训项目》 《C++》 《Linux》 《算法》 🌝 每一个不曾起舞的日子,都是对生命的辜负 目录 前言 1.什么是左值什么是右值 左值 右值 2.什么是左值引用什么是右值引用 左

    2024年04月22日
    浏览(50)
  • 【重学C++】04 | 说透C++右值引用(上)

    【重学C++】04 | 说透C++右值引用、移动语义、完美转发(上) 大家好,我是只讲技术干货的会玩code,今天是【重学C++】的第四讲,在前面《03 | 手撸C++智能指针实战教程》中,我们或多或少接触了右值引用和移动的一些用法。 右值引用是 C++11 标准中一个很重要的特性。第一

    2024年02月06日
    浏览(39)
  • c++右值引用、移动语义、完美转发

    左值:一般指的是在内存中有对应的存储单元的值,最常见的就是程序中创建的变量 右值:和左值相反,一般指的是没有对应存储单元的值(寄存器中的立即数,中间结果等),例如一个常量,或者表达式计算的临时变量 左值引用:C++中采用 对变量进行引用,这种常规的引

    2024年02月05日
    浏览(55)
  • c++积累8-右值引用、移动语义

    1.1 背景 c++98中的引用很常见,就是给变量取个别名,具体可以参考c++积累7 在c++11中,增加了右值引用的概念,所以c++98中的引用都称为左值引用 1.2 定义 右值引用就是给右值取个名字,右值有了名字之后就成了普通变量,可以像使用左值一样使用。 语法:数据类型 变量名

    2023年04月23日
    浏览(38)
  • C++右值引用(左值表达式、右值表达式)(移动语义、完美转发(右值引用+std::forward))(有问题悬而未决)

    在 C++ 中,表达式可以分为左值表达式和右值表达式。左值表达式指的是可以出现在赋值语句左边的表达式,例如变量、数组元素、结构体成员等;右值表达式指的是不能出现在赋值语句左边的表达式,例如常量、临时对象、函数返回值等。 右值是指将要被销毁的临时对象或

    2024年02月04日
    浏览(45)
  • Learning C++ No.29 【右值引用实战】

    北京时间:2023/6/7/9:39,上午有课,且今天是周三,承接之前博客,今天我又去帮我舍友签到早八,但愿这次不会被发现吧!嘻嘻嘻!并且刚刚发文有关对C++11相关知识,由于所剩时间不多,这里我们就简单的为下篇博客,当然也就是该篇博客打一打铺垫,哦!对了,今天是高

    2024年02月08日
    浏览(45)
  • C++ 学习系列 1 -- 左值、右值与万能引用

    简单的说,左值可以放在等号的左边,右值可以放在等号的右边。 左值可以取地址,右值不能取地址。 1.1 左值举例: 变量、函数或数据成员 返回左值引用的表达式 如 ++x、x = 1、cout \\\' \\\'  int x = 0 1.2 右值举例: 返回非引用类型的表达式 如 x++、x + 1 除字符串字面量之外的字面

    2024年02月14日
    浏览(44)
  • 【重学C++】04 | 说透C++右值引用、移动语义、完美转发(上)

    【重学C++】04 | 说透C++右值引用、移动语义、完美转发(上) 大家好,我是只讲技术干货的会玩code,今天是【重学C++】的第四讲,在前面《03 | 手撸C++智能指针实战教程》中,我们或多或少接触了右值引用和移动的一些用法。 右值引用是 C++11 标准中一个很重要的特性。第一

    2024年02月06日
    浏览(46)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包