C++11重大新增特性:左值引用 & 右值引用 & 移动构造 & 移动赋值

一、右值引用和左值引用概念和区别

 C++11为了支持移动操作(移动构造和移动赋值),新标准引入了新的引用类型 —— 右值引用。我们将C++11之前的引用都成为左值引用。但无论是左值引用还是右值引用,本质上都是给对象取别名!

1.1 左值 & 左值引用

 左值是一个表达式(如变量、解引用后的指针),表示的是一个对象的身份。我们可以对左值进行赋值操作、取地址。左值可以出现在等号的两边。

 评判一个表达式是否为左值的最根本标志就是:是否可以取地址。所以对于一个const修饰的变量,由于该变量可以取地址,所以也是一个典型的左值。左值引用即是左值的别名。

int main()
{
	//a、b、p、*p都是左值
	int* p = new int(0);
	int a = 10;
	const int b = 12;

	//rp、ra、rb、rval都是左值引用
	int*& rp = p;
	int& ra = a;
	const int& rb = b;
	int& rval = *p;
	return 0;
}

1.2 右值 & 右值引用

 右值也是一个表达式,和左值不同的是:右值只能出现在等号的右边(即不能被赋值),不能取地址(最根本原因),通常是字面常量、表达式返回值,函数返回值。右值引用就是对右值的引用,通过&&来获取右值引用。

右值不能取地址。但对右值取别名后,会导致右值被存储到特定的区域,并且可以取到该区域的指针。比如:字面常量10是一个右值,不能取地址。如果10被ra引用后,我们可以对ra取地址,并且可以通过修改ra进而修改右值。如果不想该右值被修改,我们可以通过const进行修饰!!

int Add(const int x, const int y)
{
	return x + y;
}

int main()
{
	//10、10 + 20、Add函数返回值都是右值,无法取地址
	//ra、rb、rc都是右值引用
	int&& ra = 10;
	int&& rb = 10 + 20;
	int&& rc = Add(1, 2);
	ra = 20;
	return 0;
}
  • 需要注意的是:上述ra、rb、rc虽然是右值引用,但ra、rb、rc本身还是一个变量,并且可以取地址,是一个左值!!

二、左值引用和右值引用对比

2.1 左值引用

  1. 普通的左值引用只能引用左值,不能引用右值。
  2. const修饰的左值引用,不仅可以引用左值,还可以引用右值!!

【示例】:

int main()
{
	int a = 10;

	int& ra = a;
	//int& rb = 10;//error,普通左值引用不能引用右值
	const int& rc = a;
	const int& rd = 10;
	return 0;
}

2.1 右值引用

  1. 右值引用可以引用右值,但不能引用左值。
  2. 我们可以通过move函数,让右值引用引用左值!!move函数调用告诉编译器:我们有一个左值,但我们希望像一个右值一样处理它。需要注意的是,move函数的返回值是一个右值,但move函数本身不会修改左值属性!

【示例】:

int main()
{
	int a = 10;
	const int&& rb = 10;

	//int&& ra = 1;//error, 右值引用无法引用左值
	//error,move本身不会修改左值属性
	//move(a);
	//int&& ra = a;

	int&& ra = move(a);//move后的返回值是一个右值
	return 0;
}

三、右值和右值引用诞生的意义

 在C++11标志之前,如果在vector、list等容器保存的是一块空间的指针。此时如果调用拷贝构造函数和拷贝赋值函数,编译器会进行一个深拷贝构造出新对象,将就对象释放。
 在很多情况下拷贝对象时无法避免的。

但如果返回的是一个临时对象会发生什么呢?

【示例】:
在这里插入图片描述

  • 我们发现如果用一个临时对象拷贝构造一个变量s时,编译器会先拷贝构造出一个临时对象,在用该临时对象出拷贝构造变量s
  • 但该过程中存在一个问题:临时对象深拷贝创建后仅仅使用一次便立即销毁、原对象ret是一个临时对象马上就要出作用域销毁,此时依旧对ret进行拷贝构造。
  • 上述情况在实际过程中会在大量场景中频繁出现,并且意义不大。这也意味着大量的无意义的深拷贝产生,将导致性能的下降。我们是否可以不进行拷贝,直接将原始数据转移到新对象中呢?(该操作的前提是原始数据马上就要被销毁)
  • 为了解决上述情况,C++11引入了移动构造函数和移动拷贝函数。移动构造函数和移动拷贝函数可以将一个待销毁的变量数据(该变量通常被编译器识别为右值)直接转移到新对象。而右值和右值引则是为实现这些函数运营而生的!!

四、移动构造 & 移动赋值

 在C++中,右值分为两种:内置定义类型右值为纯右值;自定义类型右值为将亡值!!对于纯右值,移动构造函数、移动赋值函数没有太大价值,行为和拷贝构造函数、移动构造函数类型。(上述临时对象ret虽然可以取地址是一个左值,但编译器会特殊处理将其识别为右值,即将亡值)
 只有当自定义类型中存在资源的深拷贝时,此时才能移动构造和移动赋值的价值。(直接转移资源,而非深拷贝!!)

4.1 移动构造函数

 类似于拷贝构造函数,移动构造函数的第一个参数是该类类型的引用,不同的是引用参数是一个右值。移动构造函数的本质是直接将右值对象的资源窃取过来,占为己有,此时不在进行深拷贝。所以该构造称为移动构造,用别人的资源来构造自己。

【示例:移动构造和拷贝构造函数实现和对比】:

namespace mystring
{
	class string
	{
	public:
		string(const char* str = "")//默认构造函数
			:_size(strlen(str))
			, _capacity(_size)
		{
			cout << "string(const char* str = "") ----- 构造函数" << endl;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		//拷贝构造函数
		string(const string& s)
		{
			cout << "string(const string& s) ---- 拷贝构造函数 深拷贝" << endl;
			string tmp(s._str);
			swap(tmp);
		}

		//移动构造函数
		string(string&& s)
		{
			cout << "string(string&& s) ---- 移动构造函数  移动语义" << endl;
			swap(s);
		}

		void swap(string& s)
		{
			std::swap(_str, s._str);
			std::swap(_size, s._size);
			std::swap(_capacity, s._capacity);
		}
	private:
		char* _str = nullptr;
		size_t _size = 0;
		size_t _capacity = 0;
	};
};

int main()
{
	mystring::string s("123456");
	cout << endl;

	mystring::string s1 = s;
	cout << endl;

	mystring::string s2(move(s1));
	return 0;
}

【运行结果】:
在这里插入图片描述

  • 在上述过程中,我们发现移动构造过程中没有深拷贝。原因在于移动构造直接将原资源抢占,交换过来!!

4.2 移动赋值函数

 移动赋值函数和移动拷贝函数一样,直接将将亡值的资源交换抢占过来。

【示例: 移动赋值函数和拷贝赋值函数实现和对比】:

namespace mystring
{
	class string
	{
	public:
		string(const char* str = "")//默认构造函数
			:_size(strlen(str))
			, _capacity(_size)
		{
			//cout << "string(const char* str = "") ----- 构造函数" << endl;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		//拷贝赋值函数
		string& operator=(const string& s)
		{
			string tmp(s);
			swap(tmp);
			cout << "string& operator=(const string& s) ---- 拷贝赋值函数 深拷贝" << endl;
			return *this;
		}

		//移动赋值函数
		string& operator=(string&& s)
		{
			swap(s);
			cout << "string& operator=(string&& s) ---- 移动语义" << endl;
			return s;
		}
		
		//拷贝构造函数
		string(const string& s)
		{
			string tmp(s._str);
			swap(tmp);
			cout << "string(const string& s) ---- 拷贝构造函数 深拷贝" << endl;
		}

		//移动构造函数
		string(string&& s)
		{
			swap(s);
			cout << "string(string&& s) ---- 移动构造函数  移动语义" << endl;
		}
		void swap(string& s)
		{
			std::swap(_str, s._str);
			std::swap(_size, s._size);
			std::swap(_capacity, s._capacity);
		}
	private:
		char* _str = nullptr;
		size_t _size = 0;
		size_t _capacity = 0;
	};
};

int main()
{
	mystring::string s("123456");
	cout << endl;
	
	mystring::string s1, s2;
	s1 = s;
	cout << endl;

	s2 = move(s);
	cout << endl;
	return 0;
}

【运行结果】:

在这里插入图片描述

五、完美转发(引用在模板中的用途)

5.1 模板中的万能引用(折叠引用)&&

 模板中的&&不是右值引用,表示的时万能引用,即可以接收左值也可接受右值。但万能引用只是提供了能够同时接受左值引用和右值引用的能力。
 引用类型的唯一作用就是限制接收的类型,后续使用中会退化成左值。
(这很好理解,以移动构造和移动赋值为例,其最重要的功能就是转移右值的资源。但显然右值是无法被修改的。所以当一个右值传递给移动构造和移动赋值后,后续使用会退化成左值)

【示例】:

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<class T>
void PerfectForward(T&& t)
{
	Fun(t);
}

int main()
{
	int a = 11;
	PerfectForward(a);//左值
	PerfectForward(10);//右值

	const int b = 10;
	PerfectForward(b);//const左值
	PerfectForward(move(b));//const 右值
	return 0;

【运行结果】:
在这里插入图片描述

  • 函数模板中的&&称为万能引用或折叠引用,上述代码成功运行,从侧面说明万能引用既可以接收右值还可以接收左值。同时最终结果均为左值相关,进一步说明万能引用接收右值后会退化成左值!

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

 万能引用在接受相关引用后会退化成一个左值。这个机制也是移动构造和移动赋值的基础。
 同时在C++中提出了完美转发的概念,完美转发可以保留对象在传参过程中的原生类型属性。

【示例】:

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<class T>
void PerfectForward(T&& t)
{
	Fun(forward<T>(t));//完美转发,保留t的原生属性
}

int main()
{
	int a = 11;
	PerfectForward(a);//左值
	PerfectForward(10);//右值

	const int b = 10;
	PerfectForward(b);//const左值
	PerfectForward(move(b));//const 右值
	return 0;
}

【运行结果】:
在这里插入图片描述

【完美转发实际场景中的作用】:

namespace mystring
{
	class string
	{
	public:
		string(const char* str = "")//默认构造函数
			:_size(strlen(str))
			, _capacity(_size)
		{
			cout << "string(const char* str = "") ----- 构造函数" << endl;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		//拷贝构造函数
		string(const string& s)
		{
			string tmp(s._str);
			swap(tmp);
			cout << "string(const string& s) ---- 拷贝构造函数 深拷贝" << endl;
		}

		//移动构造函数
		string(string&& s)
		{
			swap(s);
			cout << "string(string&& s) ---- 移动构造函数  移动语义" << endl;
		}

		//拷贝赋值函数
		string& operator=(const string& s)
		{
			string tmp(s);
			swap(tmp);
			cout << "string& operator=(const string& s) ---- 拷贝赋值函数 深拷贝" << endl;
			return *this;
		}

		//移动赋值函数
		string& operator=(string&& s)
		{
			swap(s);
			cout << "string& operator=(string&& s) ---- 移动语义" << endl;
			return s;
		}
		void swap(string& s)
		{
			std::swap(_str, s._str);
			std::swap(_size, s._size);
			std::swap(_capacity, s._capacity);
		}
	private:
		char* _str = nullptr;
		size_t _size = 0;
		size_t _capacity = 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 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 = nullptr;
};


int main()
{
	List<mystring::string> lt;
	cout << endl;
	lt.PushBack("11111111");
	return 0;
}

【运行结果】:

  • 调试时,lt.PushBack("11111111");走的是void Insert(Node* pos, T&& x)版本的插入,而非void Insert(Node* pos, const T& x)

在这里插入图片描述

六、新的类功能

6.1 C++11新增默认成员函数

 原来C++类中,有6个默认成员函数:(后两个作用、意义不大)

  1. 构造函数
  2. 析构函数
  3. 拷贝构造函数
  4. 拷贝赋值重载
  5. 取地址重载
  6. const 取地址重载

 在C++11中又新增两个默认成员函数:移动构造函数和移动赋值运算符重载。
 但比较特殊的是,对于移动构造函数和移动赋值运算符重载来说,只有当拷贝构造函数、拷贝赋值重载、析构函数都没显示的写时,编译器才会生成一个默认的移动构造函数和移动赋值运算符重载。
 对于默认生成的移动构造函数来说,内置类型成员按值拷贝;对于自定义类型成员,则需要看该自定义成员是否实现了移动构造。如果实现了,调用该自定义类型成员的移动构造;否则调用拷贝构造!!
 对于默认生成的移动赋值运算符重载,同上。

6.2 类成员变量初始化

 在C++11中,允许类成员变量在声明时给默认缺省值。当调用类的构造函数时,如果没有显示的传递初始值,编译器会用成员变量声明时的默认缺省值去初始化成员变量!

class string
	{
	public:
		//调用该函数时,编译器会应声明是的缺省值去初始化成员变量
		//即:_str = nullptr、_size = 0、 _capacity = 0
		string()//默认构造函数
		{ }
	private:
		char* _str = nullptr;
		size_t _size = 0;
		size_t _capacity = 0;
	};

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

 C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成。

【示例】:

class Person
{
public:
	Person(const char* name = "", int age = 0)
		:_name(name)
		, _age(age)
	{}
	Person(const Person& p)
		:_name(p._name)
		, _age(p._age)
	{}
	Person(Person&& p) = default;
private:
	mystring::string _name;
	int _age;
};
int main()
{
	Person s1;
	cout << endl;

	Person s2 = s1;
	cout << endl;

	Person s3 = std::move(s1);
	cout << endl;
	return 0;
}

【成员函数_name的相关实现如下】:

namespace mystring
{
	class string
	{
	public:
		string(const char* str = "")//默认构造函数
			:_size(strlen(str))
			, _capacity(_size)
		{
			cout << "string(const char* str = "") ----- 构造函数" << endl;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		//拷贝构造函数
		string(const string& s)
		{
			string tmp(s._str);
			swap(tmp);
			cout << "string(const string& s) ---- 拷贝构造函数 深拷贝" << endl;
		}

		//移动构造函数
		string(string&& s)
		{
			swap(s);
			cout << "string(string&& s) ---- 移动构造函数  移动语义" << endl;
		}

		//拷贝赋值函数
		string& operator=(const string& s)
		{
			string tmp(s);
			swap(tmp);
			cout << "string& operator=(const string& s) ---- 拷贝赋值函数 深拷贝" << endl;
			return *this;
		}

		//移动赋值函数
		string& operator=(string&& s)
		{
			swap(s);
			cout << "string& operator=(string&& s) ---- 移动语义" << endl;
			return s;
		}
		void swap(string& s)
		{
			std::swap(_str, s._str);
			std::swap(_size, s._size);
			std::swap(_capacity, s._capacity);
		}
	private:
		char* _str = nullptr;
		size_t _size = 0;
		size_t _capacity = 0;
	};
};

【允许结果】:
在这里插入图片描述

  • 由于在Person类中,我们已经显示的实现了拷贝构造函数,因此无法编译器无法生成默认的移动构造函数。但我们通过default关键字让编译器强制生成默认的移动构造函数。对于默认生成的移动构造函数,内置类型int _age按值拷贝;对于自定义类型mystring::string _name;调用自身的移动构造函数。
  • 如果成员变量存在如const、引用的类型时,即使default强制让编译器实现,也是错误的,无法生成。

6.4 禁止生成默认函数的关键字delete

 如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁已,这样只要其他人想要调用就会报错。(C++11之前,如果我们希望阻止拷贝的类,我们一般将相关函数声明为私有。但对于有元和成员函数来说,依旧可以访问它。所以为了阻止友元函数和成员函数进行拷贝,通常将拷贝控制成员声明为私有,但不定义)

在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。

class Person
{
public:
 Person(const char* name = "", int age = 0)
 :_name(name)
 , _age(age)
 {}
 Person(const Person& p) = delete;
private:
 mystring::string _name;
 int _age;
};
  • 10
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

独享你的盛夏

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

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

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

打赏作者

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

抵扣说明:

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

余额充值