『C++』右值引用

左值和右值


左值(lvalue)和右值(rvalue)这两个概念是从C中传承而来的,在C语言中左值指的是既能出现在等号左边也能出现在等号右边的变量(或表达式)右值指的是智能出现在等号右边的变量(或表达式)

在介绍右值引用之前,我们先来了解一下,什么是右值
我们先来看一段代码,如下:

int num = 10;

以上代码实现了对int型变量num的赋值,在这里,等号的左侧是变量num,是左值等号右侧的10是常量,是右值
那么是不是说赋值符号左边的就是左值赋值符号右边的就是右值呢
我们再来看一段代码

int a = 10;	//①
int b = a;	//②

按前面例子得到的结论来说的话,这里a是①式中的左值同时a是②式中的右值,这显然是自相矛盾的,那到底什么是左值,什么是右值呢

  • 左值能对表达式取地址、或具名对象/变量。一般指表达式结束后依然存在的持久对象。简单来说,赋值运算符左边的就是左值
  • 右值不能对表达式取地址或匿名对象。一般指表达式结束后就不再存在的临时对象注意赋值运算符右边的不一定是右值右值通常指:常量、表达式的返回值(临时变量)

C++11中右值由两个概念组成纯右值和将亡值

  • 纯右值:纯右值是C++98中右值的概念,用于识别临时变量和一些不跟对象关联的值。如:常量、一些运算表达式(1+1)等
  • 将亡值生命周期将要结束的对象。如:在值返回时的临时对象

左值引用和右值引用

了解了左值和右值之后,我们来看一下什么是左值引用,什么是右值引用
简单来说,左值引用就是对左值的引用右值引用就是对右值的引用。O(∩_∩)O哈哈~(感觉在说废话);我们前面使用的引用都是左值引用


那么,左值引用可以引用右值吗?同样的右值引用可以引用左值吗
我们写段代码来看一下

#include <iostream>

int main(){
	// 这里a为左值,10为右值
	int a = 10;

	// 对左值进行左值引用
	int& left_ref1 = a;
	// 对右值进行右值引用
	int&& right_ref1 = 10;

	// 对右值进行左值引用
	int& left_ref2 = 10;
	// 对左值进行右值引用
	int&& right_ref2 = a;

	return 0;
}

我们编译来看一下
在这里插入图片描述
从上面编译的结果来看,对右值进行左值引用和对左值进行右值引用都是不行的,我们将代码调整一下

#include <iostream>

int main(){
	// 这里a为左值,10为右值
	int a = 10;

	// 对左值进行左值引用
	int& left_ref1 = a;
	// 对右值进行右值引用
	int&& right_ref1 = 10;

	// 对右值进行左值引用
	const int& left_ref2 = 10;
	// 对左值进行右值引用
	int&& right_ref2 = std::move(a);

	return 0;
}

在这里插入图片描述
从上述运行结果,我们可以得到以下结论

  • 左值引用不能够引用右值;但是使用const修饰就可以了
    在这里插入图片描述
  • 同样的,右值引用不能够引用左值;但是对要引用的左值使用std::move()修饰就可以了
    在这里插入图片描述

右值引用书写格式

/*
*	类型&& 引用变量名字 = 实体;
*/

为什么会有右值引用右值引用有什么用
下面,我们使用一个简单的String类,我们使用这个String类来看看右值引用的作用,这里我们就不具体实现了,只是看一下效果:

#pragma once

#define _CRT_SECURE_NO_WARNINGS

#include <string.h>
#include <iostream>

// String类
class String{
public:
	// 构造函数
	String(const char* str = "")
		: _str(new char[strlen(str) + 1])
	{
		strcpy(_str, str);
	}

	// 析构函数
	~String(){
		if (_str){
			delete[] _str;
		}
	}

	// 拷贝构造函数
	String(const String& s)
		: _str(new char[strlen(s._str) + 1])
	{
		std::cout << "String(const String& s)" << std::endl;

		strcpy(_str, s._str);
	}

	// 赋值运算符重载函数
	String& operator=(const String& s){
		std::cout << "String& operator=(const String& s)" << std::endl;

		if (this != &s){
			char* temp = new char[strlen(s._str) + 1];
			strcpy(temp, s._str);
			delete[] _str;
			_str = temp;
		}

		return *this;
	}

	// +=运算符重载
	String& operator+=(const String& s){
		/*
		*	...
		*/

		return *this;
	}

	// +运算符重载
	String operator+(const String& s){
		String ret(*this);
		/*
		*	...
		*/

		return ret;
	}

private:
	char* _str;
};

我们知道,左值引用,就是我们之前说的引用解决了参数值传递的问题,和部分函数返回值问题

  • 当我们需要向一个函数传递一个参数时,如果传入的是一个自定制类型,这时候如果进行值传递的话,就是一个深拷贝,开销太大,于是有了左值引用。
  • 同样的,函数返回的如果是一个自定制类型的话,也会涉及到深拷贝问题,左值引用的引入很好的解决了这个问题
  • 但是函数返回值有时候不能使用引用传递,比如下面这个例子
    在这里插入图片描述
    这里,如果使用引用返回的话,就会出大问题,因为temp是一个局部变量函数返回之后就销毁了返回的引用自然就失效了

但是值返回的话,开销太大,怎么办呢,这时候就是我们的右值引用登场的时候了。


下面,我们来写一段代码来测试一下

#include "String.h"

int main(){
	// 使用+运算符重载函数
	std::cout << "+: " << std::endl;
	String name1 = "Fan ";
	name1 = name1 + "Zhuangyuan";

	// 使用+=运算符重载函数
	std::cout << std::endl << "+=: " << std::endl;
	String name2 = "Lin ";
	name2 += "Yingxin";

	return 0;
}

运行结果如下
在这里插入图片描述
从上述结果,我们可以看出,使用+运算符重载函数一共调用了两次拷贝构造函数,也就是两次深拷贝,还有一次赋值,也就是一共三次的深拷贝,如下:
在这里插入图片描述
在这里插入图片描述


如何对上述情况进行优化呢?事实上,对上述情况的优化不是通过右值引用来完成的,而是通过移动构造函数和移动赋值函数来完成的,移动构造函数和移动赋值函数分别是构造函数和赋值运算符重载函数的重载。
我们在模拟实现的String类中添加这两个函数

// 移动构造函数
String(String&& s){
	std::cout << "String(String&& s)" << std::endl;

	// 将传入对象的资源赋给当前对象
	_str = s._str;
	// 将传入对象置空
	s._str = nullptr;
}
// 移动赋值函数
String& operator=(String&& s){
	std::cout << "String& operator=(String&& s)" << std::endl;

	// 将传入对象的资源和当前对象的资源交换
	std::swap(_str, s._str);

	return *this;
}

上述两个函数的代码实现中,我们就可以看出,其中并没有涉及到深拷贝,只是将对资源进行了交换,所以开销很小;添加了这两个函数,我们再来运行刚才的测试程序看一下效果:
在这里插入图片描述
从运行结果可以看出,对于+运算符重载函数,只运行了一次拷贝构造函数运行了一次移动构造函数和一次移动赋值函数一共是一次深拷贝,和前面的三次深拷贝,开销要小很多

std::move

我们去官网看一下它的文档
在这里插入图片描述
从中,可以看出,它的功能就是将一个左值强制转换为右值引用通过右值引用使用该值,实现移动语义
注意:被转化的左值,其生命周期并没有随着左右值的转换而改变,即std::move转换的左值变量value不会被销毁
下面我们还是来看一段代码

#include "String.h"

int main(){
	String s1("hello, world!");

	// 使用s1的右值引用来构造s2
	String s2(std::move(s1));
	// 使用s1来构造s3
	String s3(s1);

	return 0;
}

我们来调试一下
在这里插入图片描述
我们可以看到,s1的地址为0x007857f0,我们继续往下走:
在这里插入图片描述
此时,可以看到,我们使用s1的右值引用取构造s2,结果s2的空间变为0x007857f0也就是刚才s1的空间,而s1的空间变为空,我们继续向下走:
在这里插入图片描述
此时,程序崩了,这是因为,我们使用s1去构造s3的时候,对空地址进行了访问
为什么会使用s1的右值引用取拷贝构造s2,会将s1置空,这是因为调用了移动构造函数,我们仔细看移动构造函数的内部,可以看出,移动构造函数中进行的就是空间交换的操作,因为右值引用解决的是函数返回值传值返回的深拷贝问题,而被拷贝的对象是一个局部对象,也就是一个将亡值,所以移动构造函数中直接将将亡值管理的资源拿走,让后将其置空,以此来减小开销,所以就会出现上述问题。
也就是说,我们不能像上述代码一样使用movemove更多的是用在生命周期即将结束的对象上,也就是将亡值上
注意不是move函数将其空间换走了move只是改变了属性,进而使其空间被换走。
注意:如果将移动构造函数声明为常右值引用返回右值的函数声明为常量,都会导致移动语义无法实现


C++11中默认成员函数
默认情况下,编译器会隐式生成一个(如果没有用到则不会生成)移动构造函数如果程序猿声明了自定义的构造函数、移动构造、拷贝构造函数、赋值运算符重载、移动赋值、析构函数,编译器都不会再生成默认版本。编译器生成的默认移动构造函数实际和默认的拷贝构造函数类似,都是浅拷贝。因此,在类中涉及到资源管理时,最好自己定义移动构造函数。其他类有无构造都无关紧要。
注意C++11中拷贝构造/移动构造/赋值/移动赋值函数必须同时提供,或者同时不提供,程序才能保证类同时具有拷贝和移动语义。

完美转发

我们继续使用之前的简单的String类,来段代码看一下:

#include "String.h"

void Func(String&& s){
	// 使用s去拷贝构造copy
	String copy(s);
}

int main(){
	String str("hello, world!");

	// 传递右值引用
	Func(std::move(str));

	return 0;
}

按照我们之前的说法,这里s是右值引用,所以使用s去拷贝构造copy会调用移动构造函数,我们实际来运行一下:
在这里插入图片描述
从上述运行结果可以看出,这里调用的不是移动构造函数,而是拷贝构造函数,这是因为参数在传递过程中丢失了右值属性,如果想要保持其右值属性,需要使用完美转发
C++11中使用forward<T>()函数来实现完美转发
在这里插入图片描述

#include "String.h"

void Func(String&& s){
	// 完美转发
	String copy(std::forward<String>(s));
}

int main(){
	String str("hello, world!");

	// 传递右值引用
	Func(std::move(str));

	return 0;
}

编译运行,结果如下
在这里插入图片描述
可以看到调用的是移动构造函数,证明完美转发成功

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值