• C++11(二)右值引用与移动语义+完美转发


    一、左值引用和右值引用

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

    1.1 什么是左值

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

    int main()
    {
    	// 以下的p、*p、b、c 都是左值
    	int* p = new int(0);
    	*p = 20;
    	int b = 1;
    	const int c = 2;// const很特殊
    
    	// 以下几个是对上面左值的左值引用
    	int*& rp = p;
    	int& rb = b;
    	const int& rc = c;
    	int& pvalue = *p;
    
    	return 0;
    }
    

    1.2 什么是右值

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

    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);
    
    	return 0;
    }
    

    1.3 交叉引用问题

    左值引用能否引用右值?

    // 交叉引用
    int main()
    {
    	// 以下的p、*p、b、c 都是左值
    	int* p = new int(0);
    	*p = 20;
    	int b = 1;
    	const int c = 2;
    
    
    	// 以下几个都是常见的右值
    	double x = 1.1, y = 2.2;
    	10;
    	x + y;
    	fmin(x, y);
    
    	// 左值引用不能直接引用右值
    	int& r1 = 10;
    	double& r1 = x + y;
    	double& r1 = fmin(x, y);
    
    	// 加const修饰后可以
    	const int& r1 = 10;
    	const double& r1 = x + y;
    	const double& r1 = fmin(x, y);
    	return 0;
    }
    

    右值引用能否引用左值?

    // 交叉引用
    int main()
    {
    	// 以下的p、*p、b、c 都是左值
    	int* p = new int(0);
    	*p = 20;
    	int b = 1;
    	const int c = 2;
    
    
    	// 以下几个都是常见的右值
    	double x = 1.1, y = 2.2;
    	10;
    	x + y;
    	fmin(x, y);
    
    	// 右值引用不能直接引用左值
    	int*&& rr1 = p;
    	int&& rr2 = *p;
    	int&& rr3 = b;
    
    	// 但是右值引用可以引用move以后的左值
    	int*&& rr1 = move(p);
    	int&& rr2 = move(*p);
    	int&& rr3 = move(b);
    
    	return 0;
    }
    

    注意
    需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用,是不是感觉很神奇,这个了解一下实际中右值引用的使用场景并不在于此,这个特性也不重要。

    // 注意
    int main()
    {
    	double x = 1.1, y = 2.2;
    	int&& rr1 = 10;
    	double&& rr2 = x + y;
    	const double&& rr3 = x + y;
    
    	cout << &rr1 << endl;//可以取地址
    	cout << &rr2 << endl;//可以取地址
    	rr1 = 99;			//能够赋值修改
    
    	return 0;
    }
    

    总结

    • 左值引用
    1. 左值引用只能引用左值,不能引用右值。
    2. 但是const左值引用既可引用左值,也可引用右值。
    • 右值引用
    1. 右值引用只能右值,不能引用左值。
    2. 但是右值引用可以move以后的左值。

    1.4 右值引用实用场景

    实际上右值引用的产生是为了弥补左值引用的不足

    模拟实现的string如下

    // 右值引用实用场景
    namespace sjj
    {
    	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)
    		{
    			::swap(_str, s._str);
    			::swap(_size, s._size);
    			::swap(_capacity, s._capacity);
    		}
    		// 拷贝构造
    		string(const string& s)
    			:_str(nullptr)
    		{
    			cout << "string(const string& s) -- 深拷贝" << endl;
    			string tmp(s._str);
    			swap(tmp);
    		}
    		// 赋值重载
    		string& operator=(const string& s)
    		{
    			cout << "string& operator=(string s) -- 深拷贝" << endl;
    			string tmp(s);
    			swap(tmp);
    			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
    	};
    }
    

    我们先来看看左值引用的使用场景:
    场景1:左值引用做参数

    void func1(sjj::string s)
    {}
    void func2(const sjj::string& s)
    {}
    int main()
    {
    	// 左值引用的使用场景1——传值传参
    	sjj::string str1("hello");
    	func1(str1);
    	func2(str1);
    
    	return 0;
    }
    

    func1和func2的调用我们可以看到左值引用做参数减少了拷贝,提高效率的使用场景和价值,基本完美解决所有的问题。

    场景2:左值引用做返回值

    在这里插入图片描述

    int main()
    {
    	// 左值引用的使用场景2——做返回值
    	sjj::string str1("hello");
    	func1(str1);
    	func2(str1);
    
    	str1 + '1';
    
    	return 0;
    }
    

    但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。只能解决部分问题。
    string& operator+=(char ch) 解决了问题
    string operator+(char ch) 没有解决问题(存在多次拷贝的问题)

    右值引用如何解决operator+传值返回存在拷贝(多次拷贝)的问题?

    他会在成员函数中增加一个移动构造函数。调用时,哪一个最匹配,他就会去调用最匹配的构造函数。

    C++11中将右值分为:纯右值和将亡值(临时数据)

    // 拷贝构造
    string(const string& s)
    	:_str(nullptr)
    	, _size(0)
    	, _capacity(0)
    {
    	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);
    }
    

    再举个例子:
    在这里插入图片描述
    str为临时对象,函数返回时会被释放,所以str会深拷贝一份新的,传递返回给main函数里面,但是深拷贝的代价是很大的!
    在这里插入图片描述
    编译器将str识别为右值中的将亡值,那么直接调用移动构造转移资源,不需要深拷贝了,提升了效率。

    左值引用的短板

    但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。例如:sjj::string to_string(int value)函数中可以看到,这里只能使用传值返回,传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)。
    在这里插入图片描述
    加入了移动构造后:
    在这里插入图片描述

    栈帧调用原理

    不优化的场景:
    在这里插入图片描述
    如果编译器不优化str拷贝构造临时对象,临时对象作为to_string的返回值再拷贝构造ret优化,to_string函数快结束时,返回前,直接用str去构造ret

    无法优化的场景:
    当表达式没有返回值来接收时,编译器无法优化。
    在这里插入图片描述
    str构造临时对象,临时对象作为to_string的函数调用表达式的返回值无法优化,没有优化的空间。

    移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。

    例题杨辉三角中,效率得到了极大的提升,减少了拷贝的代价!
    在这里插入图片描述

    二、移动赋值

    sjj::string类中增加移动赋值函数,再去调用sjj::to_string(1234),不过这次是将sjj::to_string(1234)返回的右值对象赋值给ret1对象,这时调用的是移动赋值。

    // 移动赋值
    string& operator=(string&& s) 
    {
    	cout << "string& operator=(string&& s) -- 移动语义" << endl;
    	swap(s);
    	return *this;
    }
    int main()
    {
    	sjj::string ret1;
    	ret1 = sjj::to_string(1234);
    	return 0;
    }
    

    这里运行后,我们看到调用了一次移动构造和一次移动赋值。因为如果是用一个已经存在的对象接收,编译器就没办法优化了。sjj::to_string函数中会先用str生成构造生成一个临时对象,但是我们可以看到,编译器很聪明的在这里把str识别成了右值,调用了移动构造。然后在把这个临时对象做为sjj::to_string函数调用的返回值赋值给ret1,这里调用的移动赋值。

    STL中的容器都新增加了移动构造和移动赋值。

    容器的插入接口都会多提供一个右值引用的版本

    // C++11
    void push_back (const value_type& val);
    void push_back (value_type&& val);
    

    sjj::string中只有拷贝构造
    在这里插入图片描述
    sjj::string中既有拷贝构造也有移动构造
    在这里插入图片描述

    三、完美转发

    模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
    模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用下面学习的完美转发。

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

    结果展示:
    在这里插入图片描述
    右值引用后变成左值了。每个参数拷贝到t中,在t的作用域中就变成了左值,属性改变了。

    std::forward完美转发 在传参的过程中保留对象原生类型属性

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

    结果展示:
    在这里插入图片描述
    现在的结果就可以匹配上了。

    完美转发实用场景

    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 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<sjj::string> lt;
    	lt.PushBack("1111");
    	
    	return 0;
    }
    

    两次不同的调用结果:
    在这里插入图片描述
    总结:只要是右值引用,往下传递给其他函数,想要保持右值属性,必须使用完美转发。

  • 相关阅读:
    Mybatis—缓存
    读数据压缩入门笔记01_数据压缩导读
    网络编程 - IP协议
    使用Redis实现文章阅读量、收藏、点赞数量记录功能
    Etcd 构建高可用Etcd集群
    【站内题解】十六道csdn每日一练Python题解
    Postgresql聚合函数string_agg() 用法
    记一次由JWT导致的绕过权限
    vue.js毕业设计,基于vue.js前后端分离教室预约系统(H5移动项目) 开题报告
    kvm webvirtcloud 如何添加直通物理机的 USB 启动U盘
  • 原文地址:https://blog.csdn.net/weixin_57675461/article/details/127054224