左值引用、右值引用详解

꧁ 大家好,我是 兔7 ,一位努力学习C++的博主~ ꧂

☙ 如果文章知识点有错误的地方,请指正!和大家一起学习,一起进步❧

🚀 如有不懂,可以随时向我提问,我会全力讲解~💬

🔥 如果感觉博主的文章还不错的话,希望大家关注、点赞、收藏三连支持一下博主哦~!👀

🔥 你们的支持是我创作的动力!⛅

🧸 我相信现在的努力的艰辛,都是为以后的美好最好的见证!⭐

🧸 人的心态决定姿态!⭐

🚀 本文章CSDN首发!✍

目录

0. 前言

右值引用

1 右值引用概念

什么是左值?什么是左值引用?

什么是右值?什么是右值引用?

2 左值与右值

总结

C++11对右值进行了严格的区分:

3 引用与右值引用比较

4 右值引用使用场景和意义(移动语义)

总结

5 右值引用引用左值及其一些更深入的使用场景分析

总结

6 完美转发

为什么右值引用引用了右值以后,在后面属性就会退化成左值?


0. 前言

        此博客为博主以后复习的资料,所以大家放心学习,总结的很全面,每段代码都给大家发了出来,大家如果有疑问可以尝试去调试。

        大家一定要认真看图,图里的文字都是精华,好多的细节都在图中展示、写出来了,所以大家一定要仔细哦~

        感谢大家对我的支持,感谢大家的喜欢, 兔7 祝大家在学习的路上一路顺利,生活的路上顺心顺意~!

右值引用

1 右值引用概念

        传统的 C++ 语法中就有引用的语法,而 C++ 中新增了的右值引用语法特性,所以从现在开始之前学的引用就叫做左值引用。无论是左值引用还是右值引用,都是给对象取别名

什么是左值?什么是左值引用?

        左值是一个表示数据的表达式(如变量名或解引用的指针),一般情况下,我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边

        定义时 const 修饰符后的左值(特殊情况),不能给它赋值,但是可以取它的地址,左值引用就是给左值的引用,给左值取别名。

什么是右值?什么是右值引用?

        左值也是一个表示数据的表达式,如:字面常量、表达式返回值,传值返回函数的返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。

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);
	cout << &rr1 << endl;

	rr1 = 20;
	cout << &rr1 << endl;

	// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
	//10 = 1;
	//x + y = 1;
	//fmin(x, y) = 1;

	return 0;
}

        我们可以看到右值引用这样是可以使用的。

        而且我们知道右值是不可以改的,但是右值引用可以修改:

        临时变量就是有人接收的话就将临时变量传过去,没有人接收就丢了,不需要存起来。

        而右值引用是开了一块空间,将这些变量存起来了,

        所以这里虽然右值不能取地址,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址。

        但是其实右值引用也不是这么用的~!

2 左值与右值

  1. 普通类型的变量,因为有名字,可以取地址,都认为是左值。
  2. const修饰的常量,不可修改,只读类型的,理论应该按照右值对待,但因为其可以取地址(如果只是 const 类型常量的定义,编译器不给其开辟空间,如果对该常量取地址时,编译器才为其开辟空间), C++11 认为其是左值。
  3. 如果表达式的运行结果是一个临时变量或者对象,认为是右值。
  4. 如果表达式运行结果或单个变量是一个引用则认为是左值。

总结

  1. 不能简单地通过能否放在=左侧右侧或者取地址来判断左值或者右值,要根据表达式结果或变量的性质判断。
  2. 能得到引用的表达式一定能够作为引用,否则就用常引用。

C++11对右值进行了严格的区分:

  1. C语言中的纯右值,比如:a+b, 100
  2. 将亡值。比如:表达式的中间结果、函数按照值的方式进行返回。

3 引用与右值引用比较

        左值引用 -> 左值、右值引用 -> 右值。

        那么   左值引用 -> 右值?  右值引用 -> 左值?

int main()
{
	int a = 10;
	int& ra1 = a;   // ra为a的别名
	// int& ra2 = 10;   // 编译失败,因为10是右值

	const int& ra2 = 10;
	const int& ra3 = 10 + 20;

	return 0;
}

        所以其实左值不能直接引用右值,因为属于权限的放大,本来不能改,引用了之后可以改了,但是 const 左值可以引用右值。

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++又添加了一个特性是 move() 。

         move(a) 之后,a 还是左值。

        所以右值引用不能引用左值,但是可以引用 move 之后的左值。

4 右值引用使用场景和意义(移动语义)

namespace twotwo
{
	class string
	{
	public:
		typedef char* iterator;
		iterator begin()
		{
			return _str;
		}

		iterator end()
		{
			return _str + _size;
		}

		string(const char* str = "")
			:_size(strlen(str))
			, _capacity(_size)
		{
			//cout << "string(char* str)" << endl;

			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		// s1.swap(s2)
		void swap(string& s)
		{
			std::swap(_str, s._str);
			std::swap(_size, s._size);
			std::swap(_capacity, s._capacity);
		}

		// 拷贝构造
		string(const string& s)
			:_str(nullptr)
		{
			cout << "string(const string& s) -- 深拷贝" << endl;

			string tmp(s._str);
			swap(tmp);
		}

		// 移动构造
		string(string&& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			cout << "string(string&& s) -- 移动构造" << endl;
			this->swap(s);
		}

		// 移动赋值 
		string& operator=(string&& s)
		{
			cout << "string& operator=(string&& s) -- 移动赋值" << endl;
			this->swap(s);

			return *this;
		}

		
		// 赋值重载(如果用到移动赋值就只能用这个,下面的用不了)
		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;
		//}

		~string()
		{
			delete[] _str;
			_str = nullptr;
		}

		char& operator[](size_t pos)
		{
			assert(pos < _size);
			return _str[pos];
		}

		void reserve(size_t n)
		{
			if (n > _capacity)
			{
				char* tmp = new char[n + 1];
				strcpy(tmp, _str);
				delete[] _str;
				_str = tmp;

				_capacity = n;
			}
		}

		void push_back(char ch)
		{
			if (_size >= _capacity)
			{
				size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
				reserve(newcapacity);
			}

			_str[_size] = ch;
			++_size;
			_str[_size] = '\0';
		}

		//string operator+=(char ch)
		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}

		const char* c_str() const
		{
			return _str;
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity; // 不包含最后做标识的\0
	};

	twotwo::string to_string(int value)
	{
		bool flag = true;
		if (value < 0)
		{
			flag = false;
			value = 0 - value;
		}

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

void func1(twotwo::string s)
{}

void func2(twotwo::string& s)
{}


int main()
{
	//twotwo::to_string(1234);
	twotwo::string ret1;
	ret1 = twotwo::to_string(1234);

	return 0;
}

左值引用的使用场景:

  1. 做参数
  2. 做返回值

         我们可以看到传值和传引用的差别是很大的,一个进行了深拷贝,一个就是传过去了个别名。深拷贝的代价是很大的。

        所以这里左值引用做参数做的很彻底,没有问题。但是做返回值却是一个短板。

        我们可以看到,如果是传值返回的话就会产生一次深拷贝,传引用是不会的,那么我们就可以无脑使用传引用么?其实是不是的,因为这里是可以使用传引用返回的,但是有些场景是不可以使用的。

        我们可以看到 to_string 不能用左值引用返回,因为函数的返回值出了作用域就不在了,就不能使用引用返回,就会存在拷贝!

        那么这时就需要用到右值引用了~!

        当然,这里可不是将传值返回直接搞成右值引用的形式就可以了,这里还需要引入移动构造

        因为拷贝构造:

        这里的 const string& 可以接收左值,也可以接收右值,我们引入移动构造就是想将右值引用到移动构造中:

        移动构造的思路就是将将亡值转移走。

        我们可以看到,现在就是调用的移动构造了,这样结构越复杂,优化的程度就越大,因为这里就相当于只将它们的指针(其实也就是两个空间)交换一下,代价是非常小的~!

        但是这里其实挺复杂的,接下来解析:

        我们在用深拷贝的时候,其实本应该两次深拷贝的,但是编译器进行了优化,所以直接拷贝给了 ret1。

         如果加入了移动构造,而且没有优化的情况下,这里应该是发生一次拷贝构造,一次移动构造。

        但是这里要清楚的是 to_string 里的 str 在 to_string  里还是一个左值,但是 C++ 编译器极度追求效率,所以在识别的时候,它会将 str 往右值去识别。所以这里即使不优化的话,也其实是两次移动构造,其实优化没优化的影响不少特别大了。

        我们看一下是不是转移了:

        这个就是它的有用场景之一。

        当没有变量接收时,有移动构造调用该移动构造,没有移动构造调用拷贝构造,也就是说,不管有没有变量接收,这里一定会拷贝(移动)到那一段内存(如果返回值占用空间小的话就是寄存器),也就是说,不管有没有变量接收,肯定是有返回值的,只是看调用方接收不接收这个返回值。

        其实移动构造在库里也是添加了的:

        那么如果有人这么写:

        这里就是一次拷贝构造+一次 operator= ,这里不会优化,因为不是一个函数,如果是构造再拷贝构造,或者是拷贝构造再拷贝构造才会被优化

        我们会发现,确实没有优化,执行了两次拷贝。

        那么我们使用移动构造:

        我们这时看到就是一次移动拷贝一次拷贝构造了。

        要避免拷贝构造,那么就可以引进一个移动赋值了,跟引入移动构造的意义是相同的~

        我们可以看到,现在就是进行了一次移动构造一次移动赋值。

        右值引用的真正意义在移动构造、移动赋值这些地方最大的价值就是跟左值引用进行区分,是左值就匹配左值引用,是右值就匹配右值引用。是右值的话就进行资源的转移,这样就大大提高了效率。

        像库里的也添加了移动赋值,这里我就不去看了,大家不放心可以去看看。

总结

        右值引用出来以后,并不是直接使用右值引用去减少拷贝,提高效率,而是指针深拷贝的类提供移动构造和移动赋值,这时这些类的对象进行传参返回或者是参数为右值时,则可以用移动构造和移动赋值,转移资源,避免深拷贝,从而提升效率。

5 右值引用引用左值及其一些更深入的使用场景分析

        我们可以看到,一个是拷贝构造,一个是移动构造,那个移动构造如果没有实现,那么就还是拷贝构造,没有问题。

         我们再使用 move 的时候,一定要知道自己在做什么,虽然我们转移了,但是被转移的左值就不能用了。

        我们可以看到这里都用到了右值引用,显然用右值引用是为了提高效率。

        我们可以看到,第一个是拷贝构造,后面三个是移动构造。

        这里是因为 s1 是左值值引用接收的,但是当链接到链表中的时候,是将 s1 里的内容拷贝到链表中的 Data 中。

        下面的是右值引用接收的,当链接到链表中的时候,是将右值移动构造到链表中的 Data 中。

        所以第一个是拷贝构造,后面三个是移动构造。

        所以,以后我们如果能构造临时对象、匿名对象,我们就构造临时对象、匿名对象,这样就可以减少拷贝提高效率啦~!

总结

        右值引用使用场景二,还可以使用在容器插入接口函数中,如果实参是右值,则可以转移它的资源,减少拷贝提高效率。

6 完美转发

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(std::forward<T>(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;
}

        模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,

         我们可以看到,正常来说,就像我红框框起来的那样,应该分别按照框起来的格式去打印。

        但是我们发现和我们想想的是不一样的,而且我们看到的都是匹配的左值,这是因为引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值。

        但是我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发。

        我们可以看到,只要用到了完美转发 std::forward<T>();就可以完美的保持它本来的属性。

namespace twotwo
{
	class string
	{
	public:
		typedef char* iterator;
		iterator begin()
		{
			return _str;
		}

		iterator end()
		{
			return _str + _size;
		}

		string(const char* str = "")
			:_size(strlen(str))
			, _capacity(_size)
		{
			//cout << "string(char* str)" << endl;

			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		// s1.swap(s2)
		void swap(string& s)
		{
			std::swap(_str, s._str);
			std::swap(_size, s._size);
			std::swap(_capacity, s._capacity);
		}

		// 拷贝构造
		string(const string& s)
			:_str(nullptr)
		{
			cout << "string(const string& s) -- 深拷贝" << endl;

			string tmp(s._str);
			swap(tmp);
		}

		// 移动构造
		string(string&& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			cout << "string(string&& s) -- 移动构造" << endl;
			this->swap(s);
		}

		// 移动赋值 
		string& operator=(string&& s)
		{
			cout << "string& operator=(string&& s) -- 移动赋值" << endl;
			this->swap(s);

			return *this;
		}


		// 赋值重载(如果用到移动赋值就只能用这个,下面的用不了)
		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;
		//}

		~string()
		{
			delete[] _str;
			_str = nullptr;
		}

		char& operator[](size_t pos)
		{
			assert(pos < _size);
			return _str[pos];
		}

		void reserve(size_t n)
		{
			if (n > _capacity)
			{
				char* tmp = new char[n + 1];
				strcpy(tmp, _str);
				delete[] _str;
				_str = tmp;

				_capacity = n;
			}
		}

		void push_back(char ch)
		{
			if (_size >= _capacity)
			{
				size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
				reserve(newcapacity);
			}

			_str[_size] = ch;
			++_size;
			_str[_size] = '\0';
		}

		//string operator+=(char ch)
		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}

		const char* c_str() const
		{
			return _str;
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity; // 不包含最后做标识的\0
	};

	twotwo::string to_string(int value)
	{
		bool flag = true;
		if (value < 0)
		{
			flag = false;
			value = 0 - value;
		}

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



template<class T>
struct ListNode
{
	ListNode* _next = nullptr;
	ListNode* _prev = nullptr;
	T _data;
};

template<class T>
class List
{
	typedef ListNode<T> Node;
public:
	List()
	{
		//_head = new Node;
		_head = (Node*)malloc(sizeof(Node));
		_head->_next = _head;
		_head->_prev = _head;
	}

	void PushBack(const T& x)
	{
		Insert(_head, x);
	}

	void PushBack(T&& x)
	{
		//cout << &x << endl;
		// 这里x属性退化为左值,其他对象再来引用x,x会识别为左值

		//Insert(_head, x);
		// 这里就要用完美转发,让x保持他的右值引属性
		Insert(_head, std::forward<T>(x));
	}

	void PushFront(T&& x)
	{
		//Insert(_head->_next, x);
		Insert(_head->_next, std::forward<T>(x));
	}

	void Insert(Node* pos, T&& x)
	{
		Node* prev = pos->_prev;
		//Node* newnode = new Node;
		//newnode->_data = std::forward<T>(x); // 关键位置
		Node* newnode = (Node*)malloc(sizeof(Node));
		new(&newnode->_data)T(std::forward<T>(x));

		// prev newnode pos
		prev->_next = newnode;
		newnode->_prev = prev;
		newnode->_next = pos;
		pos->_prev = newnode;
	}

	void Insert(Node* pos, const T& x)
	{
		Node* prev = pos->_prev;
		//Node* newnode = new Node;
		//newnode->_data = x; // 关键位置
		Node* newnode = (Node*)malloc(sizeof(Node));
		new(&newnode->_data)T(x);

		// prev newnode pos
		prev->_next = newnode;
		newnode->_prev = prev;
		newnode->_next = pos;
		pos->_prev = newnode;
	}
private:
	Node* _head;
};


int main()
{
	List<twotwo::string> lt;
	twotwo::string s1("1111");
	lt.PushBack(s1);

	lt.PushBack("1111");
	lt.PushFront("2222");


	return 0;
}

        我先说一下,如果这么写,那么这里右边的 PushBack(T&& x) 的 T&& 不是万能引用了,因为左边已经实例化出对象 lt 了,那么 PushBack(T&& x) 就只能接收右值引用了。

           我们可以看到,我们实现的最后这里调的是移动赋值,而我们前面的写的那个调用的是移动构造,这是为什么呢?

        其实 STL 内存申请不是 new 出来的,它是内存池,走的空间配置器,内存池只开了空间,也就是说这个节点是没有 new 的,如果 new 的话:

        这里因为 T 就是 string ,所以 new 的话就会调用它的构造函数,所以如果是 new 出来的,那么这里就:

        如果是 new 出来的,那么 newnode->_data 就是已经存在的对象,那就直接给它赋值了,如果是内存池出来的,内存池是只开了空间没有初始化,就相当于 malloc 出来的一样。

        所以我们实现的这里调用的是移动赋值而不是移动构造。

为什么右值引用引用了右值以后,在后面属性就会退化成左值?

         前面说过,右值是不可以取地址的,但是给右值取别名后后,会导致右值被存储到特定位置,且可以取到该位置的地址。

        也就是说可以取到地址了。

         所以这里 x 属性退化为左值,其他对象再来引用x,x会识别为左值。

        所以这里就要用到完美转发,保持它的右值属性:

         我们可以看到,这样就调用到了右值引用的 Insert 了,但是在 Insert 中也是一个右值引用,所以在我画黄线的那里也要用完美转发继续保持优质属性。也就是在右值引用中只要传参就要用完美转发。

        我们可以看到现在都是调用的移动赋值了,但是我们知道这里还是有点问题,因为这里本应该都是移动构造。所以如果我们想看到和 STL 里一样的结果,我们在代码中就不能用 new 了,我们只要将 new 换位 malloc 就可以了。

        但是这里要注意的是:

        我们 malloc 后 newnode 的 next 和 prev 都会初始化,所以我们不用处理,但是我们想把 x 构造到它这个对象上去我们就要用到 定位new 。

        在一个已经存在的对象,去调用它的构造函数。

        我们可以看到这样就可以了,和 STL 基本就是一模一样了。

          如上就是 左值引用、右值引用 的所有知识,如果大家喜欢看此文章并且有收获,可以支持下 兔7 ,给 兔7 三连加关注,你的关注是对我最大的鼓励,也是我的创作动力~!

        再次感谢大家观看,感谢大家支持!

  • 8
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

NPC Online

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值