【C++】右值引用详解(移动构造、移动赋值、万能引用和完美转发、关键字default和delete)

1、区分左右值

什么是左值?
左值是一个数据的表达式,能被取地址和赋值。
左值引用就是给左值取别名。

int main()
{
	//以下a、b、p、*p都是左值
	int a = 1;
	const int b = 2;
	int* p = new int;

	//以下ra、rb、rp、value都是左值引用
	//并且看到左值a、b、p、*p也都可以出现在赋值右边
	//所以左值既可以在赋值左边,也可以在赋值右边
	int& ra = a;
	const int& rb = b;
	int*& rp = p;
	int& value = *p;
}

左值重点:
能被取地址的才是左值。
左值既可以出现在赋值左边,也可以出现在赋值右边。

什么是右值?
右值也是一个数据的表达式,右值不能取地址,如:字面常量、表达式返回值,函数返回值等

int main()
{
	int x = 1, y = 2;
	//这种字面值常量、函数返回值、表达式都是右值
	10;
	fmin(1, 2);
	x + y;

	//右值引用只能引用右值
	int&& rra = 10;
	int&& rrm = fmin(1, 2);
	int&& rrb = x + y;
}

右值不能被取地址
右值只能放在赋值右边

左值引用能引用左值和右值,右值引用只能引用右值
左值引用是给左值取别名,右值引用是给右值取别名。

int main()
{
	int x = 1, y = 2;

	int a = 1;
	//const左值引用能引用左值
	const int& ra = a;

	//const左值引用能引用右值 本质是权限的缩小
	const int& rb = x + y;
	const int& rc = 10;

	//右值引用只能引用右值
	int&& rrd = x + y;
	//int&& rrd = a; //err 无法将右值引用绑定到左值
	
	//但是右值引用能引用move修饰的左值
	int&& rre = move(a);
}

左值引用能引用左值
const左值引用即能引用左值,也能引用右值


右值引用除了引用右值,还可以引用move修饰后的左值。

2、右值引用

左值引用的意义是什么? 函数传参/函数传返回值 – 减少拷贝

template<class T>
void func1(const T& x)
{
	// ...
}

template<class T>
const T& func2(const T& x)
{
	// ...
	return x;
}

但是左值引用没有彻底解决函数中局部对象返回的问题:

template<class T>
T func3(const T& x)
{
	T temp;
	// ...
	return temp;
}

右值引用的意义就是为了补齐这个短板

2.1 右值引用的作用

先来看一段代码

#include <iostream>
#include <assert.h>
using namespace std;
namespace test
{
	class string
	{
	public:
		typedef char* iterator;
		iterator begin()
		{
			return _str;
		}

		iterator end()
		{
			return _str + _size;
		}

		string(const char* str = "")
			:_size(strlen(str))
			, _capacity(_size)
		{
			_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)
		{
			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._str);
			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 = nullptr;
		size_t _size = 0;
		size_t _capacity = 0; // 不包含最后做标识的\0
	};

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

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

int main()
{
	test::string s1 = test::to_string(1122);
	test::string s2;
	s2 = test::to_string(2233);
	return 0;
}

在这里插入图片描述

在这里插入图片描述

左边第一种情况,str因为出作用域销毁所以在返回前拷贝了一个临时对象。
那么对于一个马上要销毁的对象,如果要提高效率,那么只对它进行资源转移(指向的改变)是不是更好,而不是对它进行拷贝。
右值引用就是一个这样的思路,对于将亡值或纯右值,可以直接对它进行资源转移,从而提高效率。

通过移动构造和移动赋值,右值引用就可以实现以下两个作用

右值引用的第一个作用:减少返回值的拷贝
将下面两个函数加入test::string类

// 移动构造
string(string&& s)
{
	cout << "string(const string& s) -- 移动拷贝" << endl;
	swap(s);
}

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

	return *this;
}

int main()
{
	test::string s1 = test::to_string(1122);
	test::string s2;
	s2 = test::to_string(2233);
	return 0;
}

在这里插入图片描述

这样第一种情况,在str返回的时候,只是简单的让两个类的成员变量交换了。
第二种情况,也是仅进行了两次类成员的交换。
这样就大大减少了拷贝,提高了效率。

右值引用的第二个作用:对于插入一些右值数据,也可以减少拷贝

#include <list>
int main()
{
	list<test::string> lt;
	test::string s("hello");
	lt.push_back(s);
	lt.push_back("hello world"); //纯右值 字面值
	lt.push_back(test::string("hello world")); //将亡值 出作用域销毁
	lt.push_back(move(s)); //对于不是将亡值或纯右值的要小心
	printf("%s\n", s); //因为是资源交换,所以结果为空
	return 0;
}

在这里插入图片描述

2.2 右值引用的左值属性

右值引用在引用右值后具有左值属性,是可以取地址可以修改

int main()
{
	int x = 1, y = 1;

	int&& rr1 = x + y;
	const int&& rr2 = x + y;

	rr1++;  //右值引用具有左值属性
	//rr2++; 这也是为什么右值引用有const原因
}

为什么要这样呢?
为了移动构造和移动赋值的实现,否则不能换资源。

那么右值引用的左值属性有什么用呢?
下面通过实现list的移动拷贝和赋值来体会

namespace test
{
	template <class T>
	struct ListNode
	{
		struct ListNode<T>* _next;
		struct ListNode<T>* _prev;
		T data;

		ListNode(const T& x = T())
			:_next(nullptr)
			, _prev(nullptr)
			, data(x)
		{}
	};

	template <class T, class Ref, class Ptr>
	struct list_iterator
	{
		typedef struct ListNode<T> node;
		typedef struct list_iterator<T, Ref, Ptr> Self;

		list_iterator(node* p)
			:_pnode(p)
		{}

		bool operator!=(Self x) const
		{
			return _pnode != x._pnode;
		}

		bool operator==(Self x) const
		{
			return _pnode == x._pnode;
		}

		Ptr operator->()
		{
			return &(_pnode->data);
		}

		Ref operator*()
		{
			return _pnode->data;
		}

		Self& operator++()
		{
			_pnode = _pnode->_next;
			return *this;
		}

		Self operator++(int)
		{
			Self tmp(*this);
			_pnode = _pnode->_next;
			return tmp;
		}

		Self& operator--()
		{
			_pnode = _pnode->_prev;
			return *this;
		}

		Self operator--(int)
		{
			Self tmp(*this);
			_pnode = _pnode->_prev;
			return tmp;
		}


		node* _pnode;
	};


	template <class T>
	class list
	{
		typedef struct ListNode<T> node;
	public:
		typedef list_iterator<T, T&, T*> iterator;
		typedef list_iterator<T, const T&, const T*> const_iterator;


		iterator begin()
		{
			return iterator(_head->_next);
		}

		iterator end()
		{
			return iterator(_head);
		}

		const_iterator begin() const
		{
			return const_iterator(_head->_next);
		}

		const_iterator end() const
		{
			return const_iterator(_head);
		}

		void initlist()
		{
			_head = new node(T());
			_head->_next = _head;
			_head->_prev = _head;

			_size = 0;
		}

		// 构造
		list()
		{
			initlist();
		}

		//拷贝构造
		template <class InputIterator>
		list(InputIterator first, InputIterator last)
		{
			initlist();

			while (first != last)
			{
				push_back(*first);
				++first;
			}
		}

		void swap(list<T>& lt)
		{
			std::swap(_head, lt._head);
			std::swap(_size, lt._size);
		}

		list(const list<T>& lt)
		{
			initlist();

			list<T> tmp(lt.begin(), lt.end());
			swap(tmp);
		}

		list& operator=(list<T> lt)
		{
			swap(lt);
			return *this;
		}

		bool empty() const
		{
			return _size == 0;
		}

		size_t size() const
		{
			return _size;
		}

		~list()
		{
			clean();
			delete _head;
			_head = nullptr;
		}

		void push_back(const T& x)
		{
			insert(end(), x);
		}

		iterator insert(iterator pos, const T& x = T())
		{
			node* newnode = new node(x);
			node* cur = pos._pnode;
			node* prev = cur->_prev;

			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;

			_size++;

			return iterator(newnode);
		}

		iterator erase(iterator pos)
		{
			assert(pos != end());

			node* cur = pos._pnode;
			node* prev = cur->_prev;
			node* next = cur->_next;
			delete cur;
			prev->_next = next;
			next->_prev = prev;

			_size--;

			return iterator(next);
		}

		void clean()
		{
			iterator it = begin();
			while (it != end())
			{
				it = erase(it);
			}
		}

	private:
		node* _head;
		size_t _size;
	};

}

int main()
{
	test::list<test::string> lt;
	test::string s1("111111");
	lt.push_back(s1);

	lt.push_back(test::string("222222"));

	lt.push_back("333333");

	return 0;
}

在没有实现接收右值的push_back前,都是深拷贝。
在这里插入图片描述
实现接收右值的push_back后,运行发现还都是深拷贝
这就是右值引用左值属性的原因,在传参给insert后其实就变成了传左值。
所以后面接收这个右值都要move一下。

void push_back(T&& x)
{
	//insert(end(), x);
	insert(end(), move(x);
}

iterator insert(iterator pos, T&& x = T())
{
	node* newnode = new node(move(x));
	node* cur = pos._pnode;
	node* prev = cur->_prev;

	prev->_next = newnode;
	newnode->_prev = prev;
	newnode->_next = cur;
	cur->_prev = newnode;

	_size++;

	return iterator(newnode);
}

ListNode(T&& x = T())
	:_next(nullptr)
	, _prev(nullptr)
	, data(move(x))
{}

最后
在这里插入图片描述

2.3 万能引用和完美转发

上面的代码可见出现了大部分的冗余代码,有没有方法能够让一种引用能接收左值引用和右值引用呢?
答案是万能引用
(C++11后,stl容器为了兼顾以前的版本,所以不得不再写一份。)

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)
{
	// t可能是左值,可能是右值
	//Fun(move(t));

	// 完美转发,保持它属性
	Fun(std::forward<T>(t));
}

int main()
{
	int x = 1;
	const int y = 1;
	const int&& rr1 = 10;
	PerfectForward(x); //int -> int& (编译器将&&折叠成&)
	PerfectForward(10); //int -> int&&
	PerfectForward(y); //const int -> const int& 经过折叠
	PerfectForward(rr1); //右值引用具有左值属性
	PerfectForward(move(y)); //const int&&

	return 0;
}

在这里插入图片描述
forward完美转发
forward转发能够保留原来属性

万能引用后的函数,再调用其它函数,参数默认又是左值属性,所以要保留属性 用forward完美转发

3、默认移动构造和默认移动赋值

原来C++类中,有6个默认成员函数:
默认构造和拷贝构造
默认析构和运算符重载
默认取地址重载和const取地址重载

C++11 新增了两个:移动构造函数和移动赋值运算符重载

当类中没有写移动构造函数,并且析构函数 、拷贝构造、拷贝赋值重载,这些都没有实现,那么编译器就会默认写一个移动构造函数。

当类中没有写移动赋值重载,并且析构函数 、拷贝构造、拷贝赋值重载,这些都没有实现,那么编译器就会默认写一个移动赋值重载。(一样的)

如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值

也很好理解,因为在设计上看当实现了析构、拷贝或者赋值重载其中一个编译器就以为用原来一套,如果写了移动构造就让编译器认为用新的一套。

class Person
{
public:
	Person(const char* name = "", int age = 0)
		:_name(name)
		, _age(age)
	{}
	
	~Person()
	{}

private:
	test::string _name;
	int _age;
};

int main()
{
	Person p1;
	Person p2(p1);
	Person p3(move(Person()));
	Person s4;
	s4 = Person();
	return 0;
}

这个时候不会默认生成移动构造和移动赋值重载,因为写了析构
在这里插入图片描述

3.1 关键字default

当实现了析构、拷贝构造或赋值重载,此时编译器不会实现默认移动构造以及默认移动赋值重载,如果此时要编译器强制实现,就可以对其函数进行default关键字标识。

强制生成默认函数的关键字default:

class Person
{
public:
	Person(const char* name = "", int age = 0)
		:_name(name)
		, _age(age)
	{}

	/*Person(Person&& p)
		:_name(std::forward<test::string>(p._name))
		, _age(p._age)
	{}*/

	// 强制生成
	Person(Person&& p) = default;
	Person& operator=(Person&& p) = default;
	
	Person(const Person& p)
		:_name(p._name)
		, _age(p._age)
	{}


	Person& operator=(const Person& p)
	{
		if(this != &p)
		{
		_name = p._name;
		_age = p._age;
		}
		return *this;
	}

	~Person()
	{}

private:
	test::string _name;
	int _age;
};

int main()
{
	Person p1;
	Person p2(p1);
	Person p3(move(Person()));
	Person s4;
	s4 = Person();
	return 0;
}

在这里插入图片描述

3.2 关键字delete

如果想要限制某些默认函数的生成,在C++11之前,是将只将声明放入私有,这样外部调用生成函数就爆错。C++11之后,只要对声明加上=delete就可以限制其生成。

下面代码如果不限制默认拷贝构造生成程序就会崩溃。

class A
{
public:
	void func()
	{
		A tmp(*this);
	}

	A()
	{}

	~A()
	{
		delete[] p;
	}

	//C++11
	A(const A& aa) = delete;
	private:
		// 只声明不实现,声明为私有 C++98
		//A(const A& aa);
private:
	int* p = new int[10];
};

int main()
{
	A aa1;
	aa1.func();
	return 0;
}
  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值