详解C++中右值引用

98中的引用

  1. 概念
  2. 特性
  3. 引用的使用场景
  4. 三种传参方式效率的比较
  5. 探索:引用的底层实现方式----->指针
    • T&------>T* const
    • const T&---->const T*const
  6. 引用和指针的区别
引用的总结

11中的右值引用

为什么要有右值引用

为了提高程序运行效率,C++11中引入了右值引用,右值引用也是别名,但其只能对右值引用

int main()
{

	//右值引用---->引用形式----->只能引用右值
	int a = 10;
	int b = 20;

	//a可以为左值,能放到=号左侧的一定是左值
	//能放到=号右侧的不一定是右值
	a = b;

	//ra对10的右值引用,ra称为对10的别名
	int&& ra = 10;

	system("pause");
	return 0;
}

左值与右值

一般认为认为:可以放在=左边的,或者能够取地址的称为左值,只能放在=右边的,或者不能取地址的称为右值,但肯定有特殊情况,不一定上述说法就完全正确

int  main()
{
	const int a = 10;

	//a = 100; a不能出现在左侧
	//a也不是右值
	//a能够取地址
	cout << &a << endl;
	//int && ra = a;
	system("pause");
	return 0;
}

左值与右值总结

左值与右值不是很好区分,一般认为:

  1. 普通类型的变量,因为有名字,可以取地址,都认为是左值
  2. const修饰的常量,不可修改,只读类型的,理论上应该按照右值对待,但因为其可以取地址(如果只是const类型常量的定义,编译器不给其开辟空间,如果对该常量取地址时,编译器才为其开辟空间),C++11认为其是左值
  3. 如果表达式的运行结果是一个临时变量或者对象,认为是右值
  4. 如果表达式运行结果或单个变量是一个引用则认为是左值
int b = 10;//为右值
int&& rb = b + 10;
int g_a = 10;

int& GetG_A()
{
	return g_a;
}

int main()
{
	GetG_A() = 10;
	//下面这行代码编译报错
	//int && rc = GetG_A();
	return 0;
}

不能简单的通过能否放在=左侧右侧或者取地址来判断左值或者右值,要根据表达式结果或变量的性质判断,比如上述:c常量

C++11中的右值

右值引用,顾名思义就是对右值的引用。C++11中,右值由两个概念组成:纯右值和将亡值。

  • 纯右值
    纯右值是C++98中右值的概念,用于识别临时变量和一些不跟对象关联的值。比如:常量、一些运算表达式(1+3)等
  • 将亡值
    声明周期将要结束的对象。比如:在值返回时的临时对象。
//将亡值
//值得方式返回
//使用ret--->创建一个临时变量---->函数运行结束栈空间被回收
int Add(int a, int b)
{
	int ret = a + b;
	return ret;
}

int main()
{
	int&& Ret = Add(10,20);
	return 0;
}

引用与右值引用的比较

//C++98引用
//1. 普通类型得引用
//2. cosnt类型的引用
int main()
{
	//98中的普通类型的引用不能引用右值
	int a = 10;
	int&ra = a;
	//int&rra = 10;	不行

	//98中的const引用既可以引用左值也可以引用右值
	const int&cral = a;
	const int& cra2 = 10;
	return 0;
}

C++11中右值引用:只能引用右值,一般情况不能直接引用左值

int main()
{
	//10纯右值,本来只是一个符号,没有具体空间
	//右值引用变量r1在定义过程中,编译器产生了一个临时变量,r1实际引用的是临时变量
	int&& r1=10;
	r1=100;
	int a=10;
	int&& r2=a; //编译失败:右值引用不能引用左值
	return 0;
}

std::move()

C++11中,std::move()函数位于 头文件中,这个函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,通过右值引用使用该值,实现移动语义。 注意:被转化的左值,其声明周期并没有随着左右值的转化而改变,即std::move转化的左值变量不会被销毁

int main()
{
	int a = 10;
	int&& ra = move(a);
	return 0;
}

右值引用的场景

class String
{
public:
	String(char* str = "")
	{
		if (nullptr == str)
			str = "";
		_str = new char[strlen(str) + 1];
		strcpy(_str, str);
	}

	String(const String& s)
		: _str(new char[strlen(s._str) + 1])
	{
		strcpy(_str, s._str);
	}

	String& operator=(const String& s)
	{
		if (this != &s)
		{
			char* pTemp = new char[strlen(s._str) + 1];
			strcpy(pTemp, s._str);
			delete[] _str;
			_str = pTemp;
		}
		return *this;
	}

	String operator+(const String&s)
	{
		char* pTemp = new char[strlen(_str) + strlen(s._str)];
		strcpy(pTemp, _str);
		strcpy(pTemp + strlen(_str), s._str);
		String strRet(pTemp);
		delete pTemp;
		return strRet;
	}

	~String()
	{
		if (_str) delete[] _str;
	}
private:
	char* _str;
};
int main()
{
	String s1("hello");
	String s2("world");
	String s3(s1 + s2);
	return 0;
}

假设编译器在返回值位置不优化
上述代码在加号运算符的重载时,是创建了临时变量,存储拼接起来的字符串,返回的时候是按值返回是通过strRet拷贝了一个临时的对象。函数体内的strRet在return后,已经被销毁。所以最后s3拷贝构造的时候是通过临时对象构造的,所以s3也要再创建一个临时变量,一但拷贝构造好之后,临时对象就释放掉了
一个刚释放一个又申请,有点多此一举。能不能不让s3再创建一个临时对象
我们可以让s3直接使用返回值的临时对象。

移动语义

C++11提出移动语义概念:将一个对象中资源移动到另外一个对象的方式

  1. 可以有效的缓解内存的压力
  2. 提高程序的运行效率,因为不需要开辟空间和释放空间
//移动构造
//移动构造参数一定不能加cosnt,资源可以转移出去,但是资源不能清空了
String(String&& s)
	:_str(s._str)
{
	s._str = nullptr;
}
  1. 移动构造函数参数不能加const类型的右值引用,因为资源无法转移而导致语义失效
  2. 在C++11中,编译器会为类默认生成一个移动构造,该移动构造为浅拷贝,因此类中涉及到资源管理时,用户必须显示定义自己的移动构造
    在C++11中,拷贝构造/移动构造/赋值/移动赋值函数必须同时提供,或者同时不提供,程序才能保证类同时具有拷贝和移动语义

auto_ptr中的资源转移

	auto_ptr<int> sp1(new  int);
	auto_ptr<int> sp2(sp1);
	//sp1资源给了sp2,但是sp1生命周期还没有到头,所以不能被使用

右值引用引用左值

通过move函数把左值转化为右值。

class Person
{
public:
	Person(char* name, char* sex, int age)
		: _name(name)
		, _sex(sex)
		, _age(age)
	{}
	Person(const Person& p)
		: _name(p._name)
		, _sex(p._sex)
		, _age(p._age)
	{}
#if 0
	Person(Person&& p)
		: _name(p._name)
		, _sex(p._sex)
		, _age(p._age)
	{}
#else
	Person(Person&& p)
		: _name(move(p._name))
		, _sex(move(p._sex))
		, _age(p._age)
	{}
	//移动赋值
	Person& operator=(Person&&p)
	{
		return *this;
	}
#endif
private:
	String _name;
	String _sex;
	int _age;
};
Person GetTempPerson()
{
	Person pp("prety", "male", 18);
	return pp;		//pp确实是右值,但是里面的成员变量是当作左值,所以用move把左值转化为右值
}
int main()
{
	Person p(GetTempPerson());
	system("pause");
	return 0;
}

用move的时机,确定当前变量是将亡值,例如函数返回值

完美转发

完美转发是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数

void Func(int x)
{
	// ......
}
template<typename T>
void PerfectForward(T t)
{
	Fun(t);
}

PerfectForward为转发的模板函数,Func为实际目标函数,但是上述转发还不算完美**,完美转发是目标函数总希望将参数按照传递给转发函数的实际类型转给目标函数,而不产生额外的开销**,上述代码中的Fun(t)只是外部t的一份拷贝,就好像转发者不存在一样。

所谓完美:

函数模板在向其他函数传递自身形参时,如果相应实参是左值,它就应该被转发为左值;如果相应实参是右值,它就应该被转发为右值。这样做是为了保留在其他函数针对转发而来的参数的左右值属性进行不同处理(比如参数为左值时实施拷贝语义;参数为右值时实施移动语义)。

C++11 通过forward函数来实现完美转发:

void Fun(int &x)	//普通类型引用
{ 
	cout << "lvalue ref" << endl; 
}
void Fun(int &&x)	//普通类型右值引用
{ 
	cout << "rvalue ref" << endl; 
}
void Fun(const int &x)	//const类型普通引用
{
	cout << "const lvalue ref" << endl; 
}
void Fun(const int &&x)	//const类型的右值引用
{ 
	cout << "const rvalue ref" << endl; 
}

//通过函数模板作为转发函数
template<typename T>
//完美转发:t是左值---->传给Fun函数,t应该也是左值
//		   t如果是右值--->传给Fun函数,t应该是右值
void PerfectForward(T &&t)
{ 
	Fun(std::forward<T>(t)); //forward把t转化为右值。
	//Fun(t);
}

int main()
{
	PerfectForward(10); // 10为右值
	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;
}

foward和move

  • move实现移动语义
  • forward实现完美转发

右值引用作用

  1. 实现移动语义
  2. 给中间临时变量取别名
  3. 实现完美转发
  4. unordered_map 中的构造和插入都有用到右值引用。vectorC++11中的emplace插入(尾插)也用到了右值引用。

pash_back;必须要把对象构造好
emplace:构造加尾插

你可能感兴趣的:(c++学习之路)