【C++】—— C++11之右值引用

右值引用

1.什么是左值和右值

  • C/C++语言中可以放在赋值符号左边的变量,即具有对应的可以由用户访问的存储单元,并且能够由用户去改变其值的量。左值表示存储在计算机内存的对象,而不是常量或计算的结果。或者说左值是代表一个内存地址值,并且通过这个内存地址,就可以对内存进行读并且写(主要是能写)操作;这也就是为什么左值可以被赋值的原因了。相对应的还有右值:当一个符号或者常量放在操作符右边的时候,计算机就读取他们的“右值”,也就是其代表的真实值。
  • 简单的来说其实赋值符号左边的叫左值(是可以被修改的值),赋值符号右边的不一定为右值(若是不可以改变的就称为右值,如:常量,表达式返回值,临时变量等)
  • 下面举个例子看看什么是左值和右值
	int a = 3;
	const int b = 5;
	a = b+2; 	//a是左值,b+2是右值
	b = a+2; 	//错!b是只读的左值但无写入权,不能出现在赋值符号左边
	(a = 4) += 28; 	//a=4是左值表达式,28是右值,+=为赋值操作符
	34 = a+2;	 //错!34是字面量不能做左值

2.左值引用和右值引用

  • 左值引用左值 int& lr1 = a; lr1 = 20;
  • 右值引用右值 int&& rr1 = 10;rr1 = 20;
  • 左值引用右值 int& lr2 = 10;不支持 const int& lr2 = 10;特例,可以支持
  • 右值引用左值 int&& rr2 = a; 不支持 int&& rr2 = move(a);特例,可以支持

3.C++11中的右值

  • 纯右值:纯右值是C++98中的右值概念,用于识别临时变量和一些不跟对象关联的值。比如:常量,一些运算表达式(1+3)等。
  • 将亡值:生命周期将要结束的对象,比如:值返回时创建的临时变量

4.右值引用的应用

  • 我们先来看看STL中的string类,我们知道如果一个类中涉及到资源管理,用户必须显式提供拷贝构造、赋值运算符重载以及析构函数,否则编译器将会自动生成一个默认的,如果我们遇到返回一个临时对象时,我们应该怎么处理,才能提高效率,我们来看看。
class String
{
public:
	String(char* str = "")
	{
		if (nullptr == str)
			str = "";
		_str = new char[strlen(str) + 1];
		strcpy(_str, str);
	}

	// 拷贝构造
	// String s(左值对象)
	String(const String& s)
		: _str(new char[strlen(s._str) + 1])
	{
		cout << "String(const String& s)" << endl;

		strcpy(_str, s._str);
	}
	
	// s1 = s2 正常赋值
	String& operator=(const String& s)
	{
		cout << "String& operator=(const String& s)" << endl;
		if (this != &s)
		{
			char* pTemp = new char[strlen(s._str) + 1];
			strcpy(pTemp, s._str);
			delete[] _str;
			_str = pTemp;
		}
		return *this;
	}

	~String()
	{
		if (_str) delete[] _str;
	}
	
	const char* c_str()
	{
		return _str;
	}


private:
	char* _str;
};

移动语义

  • 假设现在有一个函数,返回值为一个String类型的对象:
    拷贝构造
  • 上述代码看起来没有什么问题,但是有一个不太尽人意的地方:GetString函数返回的临时对象,将s2拷贝 构造成功之后,立马被销毁了(临时对象的空间被释放),再没有其他作用;而s2在拷贝构造时,又需要分配 空间,一个刚释放一个又申请,有点多此一举。那能否GetString返回的临时对象的空间直接交给s2呢? 这样s2也不需要重新开辟空间了,代码的效率会明显提高。

移动语义

  • 将一个对象中资源移动到另一个对象中的方式,称之为移动语义。在C++11中如果需要实现移动语义,必须使用右值引用

移动构造

  • 我们来理解一下移动构造,看代码其实很好理解,就是将一个对象的资源移动给另一个对象,并将该对象置空。
  • 为什么要这么做呢,我们刚刚在上面提到将亡值这个概念,什么是将亡值,就是生命周期马上要结束的值,那按照我们一般的拷贝构造,在返回一个临时对象时,会进行深拷贝,这里的临时对象就是一个将亡值,我们对其进行深拷贝是一种浪费资源和浪费时间的行为,那我们用这个移动构造,直接将将亡值的资源移动到新的对象中,减少了拷贝,提高了效率。
	// 移动构造
	// String s(将亡值对象) 
	String(String&& s)
		:_str(nullptr)
	{
		cout << "String(String&& s)" << endl;
		swap(_str, s._str);
	}

移动赋值

  • 移动赋值和移动构造一样理解,也是为了减少拷贝。
	// 移动赋值
	// s1 = s2
	String& operator=(String&& s)
	{
		cout << "String& operator=(String&& s)" << endl;
		swap(_str, s._str);

		return *this;
	}

左值引用

  • 重载运算符+=体现了左值引用,我们来看看具体是怎么实现的。
	// s1 += s2  体现左值引用,传参和传值的位置减少拷贝,提高效率  
	String& operator+=(const String& s)
	{
		//this->Append(s.c_str());
		return *this;
	}

左值引用

右值引用

  • 重载运算符+,体现了右值引用,我们也来看一下是怎么实现的。
	// s1 + s2 体现右值引用,将亡值tmp自动调用移动构造和移动赋值以减少拷贝
	String operator+(const String& s)
	{
		String tmp(*this);
		//tmp.Append(s.c_str());
		return tmp;
	}

右值引用

  • 上述例子我们很容易看出,右值引用的出现也是为了减少拷贝,提高效率,而且它的出现并未改变重载运算符的实现,而是在返回一个临时对象,判断其为将亡值从而自动调用移动构造和移动赋值来减少深拷贝,以提高效率

5.完美转发

  • 完美转发是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。
  • 简单的来说,就是完美转发可以保持数据的原来属性,原来是左值经过完美转发之后还是左值,原来是右值完美转发还是右值。
  • 举个例子
void Fun(int &x){ cout << "lvalue ref" << endl; }
void Fun(int &&x){ cout << "rvalue ref" << endl; }
//void Fun(const int &x){ cout << "const lvalue ref" << endl; }
//void Fun(const int &&x){ cout << "const rvalue ref" << endl; }

template<typename T>
void PerfectForward(T &&t){ Fun(std::forward<T>(t)); }

int main()
{
	PerfectForward(10); // rvalue re
	int a;
	PerfectForward(a);			   // lvalue ref
	PerfectForward(std::move(a)); // rvalue ref

	//const int b = 8;
	//PerfectForward(b); // const lvalue ref
	//PerfectForward(std::move(b)); // const rvalue ref

	return 0;
}
发布了167 篇原创文章 · 获赞 175 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/chenxiyuehh/article/details/93665922