C++11——右值引用、移动语义、完美转发、新的类功能

目录

​​​​​​一、什么是左值和右值?

​​​​​​二、什么是左值引用和右值引用?

三、右值引用使用场景和意义

1.补齐左值引用的短板——函数传返回值时的拷贝

1.1纯右值、将亡值

纯右值

将亡值

2.移动语义

2.1移动构造

2.2移动赋值

3.插入接口右值版本

总结: 

四、完美转发

1.前置知识

2.概念

3. 完美转发的局限性

五、新的类功能


​​​​​​一、什么是左值和右值?

        左值准确来说是:一个表示数据的表达式(如变量名或解引用的指针),且可以获取他的地址(取地址),可以对它进行赋值;它可以在赋值符号的左边或者右边。

        右值准确来说是:一个表示数据的表达式(如字面常量、函数的返回值、表达式的返回值),且不可以获取他的地址(取地址);它只能在赋值符号的右边。右值也是通常不可以改变的值。

int main()
{
	// 以下的p、b、c、*p都是左值
	int a = 1;// a是左值,1作为普通字面量是右值
	const int b = 2;
	int* p = new int(0);

	double x = 1.1, y = 2.2;
	// 以下几个都是常见的右值
	10;
	x + y;
	fmin(x, y);

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

​​​​​​二、什么是左值引用和右值引用?

左值引用:给左值取别名

右值引用:给右值取别名

需要注意的是:

  • 左值引用只能引用左值;const左值引用可以左值,也可以引用右值(因为右值通常是不可以改变的值,所以用const左值引用是可以的);
  • 右值只能引用右值;左值可以通过move(左值)来转化为右值,继而使用右值引用。
	//int& ref1 = (x + y); - 权限放大
	// 表达式返回的是临时对象,具有常性
	const int& ref2 = (x + y); // 左值引用给右值取别名需加const

	// 右值引用可以给move后的左值去别名
	//int&& ref3 = a;
	int&& ref3 = move(a);
}



void func(int& a){ cout << "void func(int& a)" << endl;}
void func(int&& a){ cout << "void func(int&& a)" << endl;}

int main()
{
	int a = 1, b = 2;
	func(a);
	func(a + b);
	// 没有右值引用前,第一个func函数形参变为const int& a既可以接收右值也可以接收左值
	// 但有个问题,函数识别不了传进来的是左值还是右值
	return 0;
}

此时我们已经了解了左值和左值引用,右值和右值引用。所以可以发现,左值引用就是我们通常使用的引用。那么左值引用和右值引用的意义或者区别在哪里呢?我们继续往下看。


三、右值引用使用场景和意义

左值引用的意义在于:

  • 函数传参:实参传给形参时,可以减少拷贝。
  • 函数传返回值时,只要是出了作用域还存在的对象,那么就可以减少拷贝。

但是左值引用却没有彻底的解决问题:函数传返回值时,如果返回值是出了作用域销毁的(出了作用域不存在的),那还需要多次的拷贝构造,导致消耗较大,效率较低。

所以这也就是为什么出现了右值引用,当然这是是右值引用价值中的一个!

1.补齐左值引用的短板——函数传返回值时的拷贝

首先我们需知道:

1.1纯右值、将亡值

纯右值和将亡值都属于右值。

纯右值

运算表达式产生的临时变量、不和对象关联的原始字面量、非引用返回的临时变量、lambda表达式等都是纯右值。

举例:

  • 除字符串字面值外的字面值
  • 返回非引用类型的函数调用
  • 后置自增自减表达式i++、i--
  • 算术表达式(a+b, a*b, a&&b, a==b等)
  • 取地址表达式等(&a)
将亡值

        将亡值是指C++11新增的和右值引用相关的表达式,通常指将要被移动的对象、T&&函数的返回值、std::move函数的返回值、转换为T&&类型转换函数的返回值,将亡值可以理解为即将要销毁的值,通过“盗取”其它变量内存空间方式获取的值,在确保其它变量不再被使用或者即将被销毁时,可以避免内存空间的释放和分配,延长变量值的生命周期,常用来完成移动构造或者移动赋值的特殊任务。

通过自己模拟实现的string帮助我们理解:

namespace yrj
{
	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;
		}

        // s1 = 将亡值
		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;
		}

		string operator+(char ch)
		{
			string tmp(*this);
			tmp += ch;
			return tmp;
		}

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

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

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

2.移动语义

移动语义,在程序喵看来可以理解为转移所有权,之前的拷贝是对于别人的资源,自己重新分配一块内存存储复制过来的资源,而对于移动语义,类似于转让或者资源窃取的意思,对于那块资源,转为自己所拥有别人不再拥有也不会再使用,通过C++11新增的移动语义可以省去很多拷贝负担,怎么利用移动语义呢,是通过移动构造函数和移动赋值函数

2.1移动构造

直接上图:

这张图也就解释了移动构造也就是将原数据资源转移给其他变量

string的移动构造:

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

举个例子:

   	yrj::string s1("hello world");

	yrj::string ret1 = s1;
	yrj::string ret2 = (s1+'!');

我们也可以将move后的左值(也就是右值)赋给左值,但需注意的是:除非你想把你原先的数据给搞没,否则不要随意使用move

yrj::string ret3 = move(s1);

再来看看98跟11的区别:

编译器会自动优化(连续的构造,但是不是所有的情况都优化),将两个拷贝构造优化为一个拷贝构造,直接跳过中间的临时变量,但是对于自定义类型时,虽然将两次拷贝构造优化为一次,拷贝构造仍然要消耗很大的空间,所以这时右值引用的第一个价值就要登场!

右值引用来补齐函数传返回值时的拷贝短板,当调用拷贝构造时,之前我们只有传左值,进行深拷贝,完成拷贝构造,但现在我们有了右值,可以传右值,那么传右值的拷贝构造是怎么搞的呢?

对于左值,我们后续还要使用,所以只能进行深拷贝,完成拷贝构造。但对于右值(将亡值),可以直接进行资源的交换,将this和将亡值交换资源。

在有了移动构造以后,再经过编译器的优化,就可以做到直接移动构造(资源的交换),实现0拷贝,效率极高!!

2.2移动赋值

string的移动赋值:

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

	return *this;
}

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


注意:移动语义仅针对于那些实现了移动构造函数的类的对象,对于那种基本类型int、float等没有任何优化作用,还是会拷贝,因为它们实现没有对应的移动构造函数。


3.插入接口右值版本

C++11以后,STL所有的容器都增加了移动构造,并且STL所有的容器插入数据接口函数都增加了右值引用版本

list的接口:

我们再来看看98跟11的区别:

int main()
{
	list<yrj::string> lt;

	yrj::string s1("good morning");
	lt.push_back(s1);
	lt.push_back(move(s1));

	lt.push_back(yrj::string("good morning"));// 匿名对象也是右值
	lt.push_back("good morning");
}

 98插入数据就是老老实实的进行深拷贝:

11也就是资源转换,将将亡值和新节点中的数据进行资源交换,也可以理解为掠夺别人的资源

总结: 

左值引用减少拷贝,提高效率右值引用也是减少拷贝,提高效率。
但是他们的角度不同,左值引用是直接减少拷贝;
右值引用是间接减少拷贝,识别出是左值还是右值,如果是右值,则不再深拷贝,直接移动拷贝,提高效率。


四、完美转发

1.前置知识

需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可

以取到该位置的地址。(右值被右值引用以后就成为了左值

引用折叠:编译器不允许我们写下类似int & &&这样的代码,但是它自己却可以推导出int & &&代码出来。它的理由就是:我(编译器)虽然推导出T为int&,但是我在最终生成的代码中,利用引用折叠规则,将int & &&等价生成了int &。推导出来的int & &&只是过渡阶段,最终版本并不存在,所以也不算破坏规定。引用折叠的规则如下:

  • & + & -> &
  • & + && -> &
  • && + & -> &
  • && + && -> &&

万能引用:对于函数模板中使用右值引用的参数来说,它既可以接收右值,也可以接收左值,这个情况下的右值引用也称为万能引用。

PerfectForward(10):10是右值,模板中T &&t这种为万能引用,右值10传到PerfectForward函数中变成了右值引用,但是调用Fun()时候,t变成了左值,因为它变成了一个拥有名字的变量 ,在继续调用Fun时,还是会因为属性导致结果并不是我们需要的:

所以我们就需一个东西来保持参数原本的属性——完美转发

2.概念

完美转发指可以写一个接受任意实参的函数模板,并转发到其它函数,目标函数会收到与转发函数完全相同的实参,转发函数实参是左值那目标函数实参也是左值,转发函数实参是右值那目标函数实参也是右值。那如何实现完美转发呢,答案是使用std::forward()。

std::forward 是一个实现完美转发的关键工具,它的作用是将参数的类型和值类别原封不动地传递给其他函数。std::forward 本质上是一个条件转换为右值引用的函数模板,当参数是左值引用时,它返回一个左值引用;当参数是右值引用时,它返回一个右值引用。std::forward 完美转发在传参的过程中保留对象原生类型属性例如:

3. 完美转发的局限性

虽然完美转发可以大大提高参数传递的性能和准确性,但它也有一些局限性。首先,对于不支持移动语义的类型,完美转发无法带来性能优势。其次,完美转发可能导致代码变得复杂且难以阅读。因此,在使用完美转发时,需要权衡优势和劣势,根据实际情况进行选择。


五、新的类功能

1.默认成员函数

原来C++ 类中,有 6 个默认成员函数:
1. 构造函数
2. 析构函数
3. 拷贝构造函数
4. 拷贝赋值重载
5. 取地址重载
6. const 取地址重载
最后重要的是前4 个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。
C++11 新增了两个:移动构造函数和移动赋值运算符重载。
针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:
  • 如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任 意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类 型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造, 如果实现了就调用移动构造,没有实现就调用拷贝构造。
  • 如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中 的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内 置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋 值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造 完全类似)
  • 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
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& operator=(const Person& p)
	//{
	//	if(this != &p)
	//	{
	//		_name = p._name;
	//		_age = p._age;
	//	}
	//	return *this;
	//}

	//~Person(){}
private:
	yrj::string _name;
	int _age;
};
int main()
{
	Person s1;
	Person s2 = s1;
	Person s3 = std::move(s1);

	Person s4;
	s4 = std::move(s2);
	return 0;
}

我们没写构造的话,会自己生产调用string的移动拷贝跟移动赋值,内置类型就会按照值进行拷贝

移动构造:s1资源被转移了,自己也一无所有了

 移动赋值:s2跟s4资源实现了交换

我们再看看若自己实现了某一个函数(例如:析构函数)跟默认生成有什么区别:

可以看到自己实现了析构函数对象全部都调用的是深拷贝,其效率跟代价有点大,而默认生成却自己调用了移动函数,效率大大滴提升了。

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

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

Person(Person&& p) = default;

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

如果能想要限制某些默认函数的生成,在C++98 中,是该函数设置成 private ,并且只声明补丁
已,这样只要其他人想要调用就会报错。在 C++11 中更简单,只需在该函数声明加上 =delete
可,该语法指示编译器不生成对应函数的默认版本,称 =delete 修饰的函数为删除函数。比如:我们不想生成拷贝构造函数:
Person(const Person& p) = delete;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值