【C++】右值引用和移动语义

C++11中新增了右值引用语法特性,所以之前所学的引用均为左值引用。无论是左值引用还是右值引用,都是给对象取别名。
左值和左值引用

左值是一个表示数据的表达式(如变量名或解引用的指针),**我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。**定义时const修饰符后的左值,不能给它赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。

cpp 复制代码
int main()
{
	int* p = new int(0);
	int b = 1;
	const int c = 2;
	//以上的p,b,c,*p都是左值

	int*& rp = p;
	int& rb = b;
	const int& rc = c;
	int& pvalue = *p;
	//以上几个是对上面左值的左值引用
	return 0;
}

右值和右值引用

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

cpp 复制代码
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:"=":左操作数必须为左值
	10 = 1;
	x + y = 1;
	fmin(x, y) = 1;

	return 0;
}

右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用。不过这个特性不重要。

左值引用与右值引用比较

左值引用相关:

复制代码
1.左值引用只能引用左值,不能引用右值。
2.但const左值引用既可引用左值,也可引用右值。

右值引用相关:

复制代码
1.右值引用只能引用右值,不能引用左值。
2.但右值可以引用move以后的左值。

右值引用的使用场景和意义

cpp 复制代码
namespace Q
{
	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(const char* str = "")" << endl;
			_str = new char[_capacity+1];
			strcpy(_str, str);
		}
		void swap(string& s)
		{
			std::swap(s._str,_str);
			std::swap(s._size,_size);
			std::swap(s._capacity,_capacity);
		}
		string(const string& s)
			: _size(s._size)
			, _capacity(s._capacity)
		{
			_str = new char[_capacity + 1];
			strcpy(_str, s._str);
			cout << "string(const string& s)--深拷贝" << endl;
		}

		string(string&& s)
			:_str(nullptr)
			,_size(0)
			,_capacity(0)
		{
			cout << "string(string&& s)--移动拷贝" << endl;
			swap(s);
		}
		string& operator=(const string& s)
		{
			cout << "string& operator=(const 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()
		{
			cout << "~string()" << endl;
			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;
	};
	string to_string(int value);
}
//Q::string to_string(int value)
Q::string Q::to_string(int value)
{
	Q::string str;
	//...
	return str;
}

左值引用的使用场景:

做参数和返回值都可以提高效率

cpp 复制代码
void func1(Q::string s)
{ }
void func2(const Q::string& s)
{ }
int main()
{
	Q::string s1("hello world");
	func1(s1);
	func2(s1);
	//func1和func2对比可看到左值引用做参数减少了拷贝,提高效率。

	//string operator+=(char ch)
	//传值返回存在深拷贝
	//string& operator+=(char ch)
	//传左值引用 没有拷贝 提高了效率
	s1+='&';
	return 0;
}

左值引用短板:

当函数返回对象是一个局部变量时不能使用左值引用返回,只能传值返回。传值返回会导致至少一次拷贝构造(旧一点的编译器可能是两次拷贝构造)。

本来应该是两次拷贝构造,但是新一点的编译器一般会优化,优化后变成一次拷贝构造。
to_string的返回值是一个右值,用这个右值构造ret1,如果没有移动构造,调用就会匹配调用拷贝构造,因为const左值引用是可以引用右值的,这里就是一个深拷贝。
右值引用和移动语义解决上述问题:

在Q::string中增加移动构造,移动构造本质是将右值的资源窃取过来占为己有,不用做深拷贝了,所以叫做移动构造,就是利用资源转移来构造自己。

所以场景1:自定义类型中深拷贝的类必须传值返回的场景。

cpp 复制代码
string(string&& s)
	:_str(nullptr)
	,_size(0)
	,_capacity(0)
{
	cout << "string(string&& s)--移动拷贝" << endl;
	swap(s);
}
int main()
{
	Q::string ret1 = Q::to_string(-366);
	return 0;
}

再运行上面的代码,会发现调用了移动构造。

cpp 复制代码
string& operator=(string&& s)
{
	cout << "string& operator=(string&& s)--移动赋值" << endl;
	swap(s);
	return *this;
}
int main()
{
	Q::string ret1;
	ret1 = Q::to_string(-366);
	return 0;
}
// 运行结果:
// string(string&& s) -- 移动拷贝
// string& operator=(string&& s) -- 移动赋值

运行后调用了一次移动构造和一次移动赋值。如果是用一个已经存在的对象接收,编译器没办法优化。Q::string函数会先用str构造生成一个临时对象,这里编译器将str识别成右值,调用了移动构造。然后将临时对象作为Q::string函数调用的返回值赋值给ret1,这里调用的是移动赋值。

右值引用引用左值及其它使用场景分析

当需要右值引用去引用一个左值时,可通过move函数将左值转化为右值。

cpp 复制代码
int main()
{
	Q::string s1("hello world");
	Q::string s2(s1);
	Q::string s3(move(s1));
//这里调用移动构造,但一般不要这样用,因为s1的资源被转移给了s3,s1被置空了。
	return 0;
}


场景二:容器的插入接口,如果插入对象是右值,可利用移动构造转移给数据结构中的对象,也减少拷贝提高效率。

完美转发

模板中的&&万能引用

复制代码
模板中的&&不代表右值引用,而是万能引用,既能接收左值又能接收右值。
但引用模板的唯一左右就是限制了接收的类型,后续使用中都退化成了左值。
我们希望它在传递的过程中保持它的左值或者右值属性,需要用完美转发。
cpp 复制代码
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(move(a)); // 右值
	const int b = 8;
	PerfectForward(b);// const 左值
	PerfectForward(move(b)); // const 右值
	return 0;
}

完美转发实际中的使用场景:

cpp 复制代码
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->_next = _head;
		_head->_prev = _head;
	}
	void PushBack(T&& x)
	{
		//Insert(_head, 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); // 关键位置
		// 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; // 关键位置
		// prev newnode pos
		prev->_next = newnode;
		newnode->_prev = prev;
		newnode->_next = pos;
		pos->_prev = newnode;
	}
private:
	Node* _head;
};
int main()
{
	List<Q::string> lt;
	lt.PushBack("1111");
	lt.PushFront("2222");
	return 0;
}
相关推荐
Demon--hx4 小时前
[C++]迭代器
开发语言·c++
BanyeBirth4 小时前
C++窗口问题
开发语言·c++·算法
q***06295 小时前
PHP进阶-在Ubuntu上搭建LAMP环境教程
开发语言·ubuntu·php
郝学胜-神的一滴8 小时前
Qt的QSlider控件详解:从API到样式美化
开发语言·c++·qt·程序人生
橘颂TA8 小时前
【剑斩OFFER】算法的暴力美学——连续数组
c++·算法·leetcode·结构与算法
学困昇9 小时前
C++11中的{}与std::initializer_list
开发语言·c++·c++11
郝学胜-神的一滴9 小时前
Qt的QComboBox控件详解:从API到样式定制
开发语言·c++·qt·程序人生·个人开发
憧憬blog9 小时前
【Kiro开发集训营】拒绝“屎山”堆积:在 Kiro 中重构“需求-代码”的血缘关系
java·开发语言·kiro
青春:一叶知秋9 小时前
【Redis存储】List列表
数据库·redis·缓存
n***i9510 小时前
Java NIO文件操作
java·开发语言·nio