【C++】左值引用和右值引用

C++98中提出了引用的概念,左值引用。C++11中新增了右值引用语法特性。

无论左值引用还是右值引用,都是给对象取别名。


什么是左值和右值呢?

=左边的值就是左值?=右边的值就是右值?

其实这是一个C语言就留下来的语法坑。像位运算的左移和右移一样。左右并不是指方向。

左边的值不一定是左值,右边的值也不一定是右值。

1. 左值引用与右值引用

1.1 左值与左值引用

左值是一个数据表达式,如:变量名、解引用的指针变量。可以对它取地址和赋值,可以出现在赋值符号的左边和右边,左值通常为变量。 

//a、p、*p、pvalue、b都是左值
int a = 1;
int* p = new int(10);
int pvalue = *p;
const int b = 2;//const修饰的左值

左值引用就是对左值的引用。

//对上面左值的引用
int& ra = a;
int* & rp = p;
int& rpvalue = pvalue;
const int& rb = b;

1.2 右值与右值引用

右值也是一个数据表达式,如字面常量、表达式返回值、函数返回值(不能是左值引用返回)等。右值可以出现在赋值符号的右边,但不能出现在赋值符号的左边, 右值通常为常量

//10、a+b、Sum(a+b)、ret都是右值
int a = 1;
int b = 2;
10;
a + b;
Sum(a, b);
//右值出现在赋值符号的左边会报错。C2106 "=":左操作数必须为左值
10 = 1;
a + b = 1;
Sum(a, b) = 1;

 右值引用就是对右值的引用。

//对上面右值的引用
int&& rr1 = 10;
int&& rr2 = a + b;
int&& rr3 = Sum(a, b);

注意:

右值不能直接取地址,但是对右值引用(取别名)之后,右值将被存储到特定位置,且可以取到该位置的地址。

例如:字面量10不能直接取地址,但是rr1是可以取地址的,且可以修改,如果不想被修改,可以用const int&& rr1引用。

int a = 1;
int b = 2;
//&10;//报错。C2101 常量上的"&"
int&& rr1 = 10;
rr1 = 20;
const int&& rr2 = a + b;
//rr2 = 1;//报错。C3892 不能给常量赋值

2. 左值引用与右值引用比较

2.1 左值引用总结

  • 左值引用可以引用左值,不能引用右值。
  • 但是const左值引用既可以引用左值,又可以引用右值。
int a = 10;
int& ra1 = a;
//int& ra2 = 10;//报错。message:无法将右值引用绑定到左值
const int& ra3 = a;
const int& ra4 = 10;

2.2 右值引用总结

  • 右值引用只能引用右值,不能引用左值。
  • 但是右值引用可以引用move以后的左值。
int a = 10;
int&& ra1 = 10;
//int&& ra2 = a;//报错。message:无法将左值绑定到右值引用
int&& ra3 = std::move(a);
//std::move()可以将左值强制转换为右值。

再来看以下代码。

template<class T>
void f(const T& a)
{
	cout << "void f(const T& a)" << endl;
}
template<class T>
void f(const T&& a)
{
	cout << "void f(const T&& a)" << endl;
}
int main()
{
	int a = 10;
	f(a);//这里会匹配左值引用参数的f
	f(10);//这里会匹配右值引用参数的f
	return 0;
}

如果没有函数参数为右值引用的f()时,那么“f(10)”也会调用函数参数为左值引用的f()。因为const左值引用也可以引用右值。但是如果有函数参数为右值引用的f()时,就会调用参数最匹配的那个。

3. 移动构造和移动赋值

既然const左值引用可以引用左值也可以引用右值,那C++11提出来右值引用到底有什么作用呢?

右值又分为纯右值和将亡值

纯右值:基本类型的常量或表达式

将亡值:自定义类型的临时对象。

C++提出来右值引用,对于纯右值也就是内置类型,做右值引用是没有意义的。主要是关注右值的将亡值,也就是自定义类型的临时对象,来做资源转移。

class String
{
public:
	String(const char* str="")
		:_str(nullptr)
	{
		_str = new char[strlen(str) + 1];
		strcpy(_str, str);
	}
	String(const String& s)
	{
		cout << "String(const String& s)--深拷贝" << endl;
		_str = new char[strlen(s._str) + 1];
		strcpy(_str, s._str);
	}
	~String()
	{
		if (_str)
		{
			delete[] _str;
			_str = nullptr;
		}
	}
private:
	char* _str;
};

String f(const char* str)
{
	String tmp(str);
	return tmp;//传值返回。这里不会返回tmp,实际是返回拷贝tmp的临时对象
}

int main()
{
	String s1("左值");
	String s2(s1);//参数是左值
	String s3(f("右值-将亡值"));	//参数是右值-将亡值(传递给你用,用完我就析构了)
	String s4(std::move(s1));
    return 0;
}

这里,s2、s3的参数无论是左值还是右值,实际中都会调用深拷贝,代价太大了。

如何针对参数为左值和右值的做不同的处理呢?

3.1 移动构造

//拷贝构造
String(const String& s)
{
	cout << "String(const String& s)--深拷贝" << endl;
	_str = new char[strlen(s._str) + 1];
	strcpy(_str, s._str);
}
//移动构造
String(String&& s)
	:_str(nullptr)
{
	cout << "String(String&& s)--移动语义" << endl;
	swap(_str, s._str);
}

右值引用和移动语义来解决

新增一个参数为右值引用的构造函数,传过来的是一个将亡值,反正都要“将亡了”,不如直接把空间和值给“我”。这就做到了资源转移,并没有做深拷贝,效率高,所以叫移动构造。

移动构造本质就是将参数右值的资源窃取过来构造自己,那么就不用做深拷贝了。

在有些场景下,需要用右值引用去引用左值,去实现移动语义。那么std::move()函数会将左值强制转换为右值,但是,需要注意的是move也会将原有的资源转移,所以要慎用。 

3.2 移动赋值

//拷贝赋值
String& operator=(const String& s)
{
	cout << "String operator=(const String& s)--深拷贝" << endl;
	if (this != &s)
	{
		char* newstr = new char[strlen(s._str) + 1];
		strcpy(newstr, s._str);
		delete[] _str;
		_str = newstr;
	}
	return *this;
}
//移动赋值
String& operator=(String&& s)
{
	cout << "String operator=(String&& s)--移动赋值" << endl;
	swap(_str, s._str);
	return *this;
}

所有涉及深拷贝的容器(string/vecotr/list/map/set...)实现都可以加两个右值引用做参数的移动构造和移动赋值来减少拷贝次数,提高效率。

STL中的容器C++11都是新增了移动构造和移动赋值。

4. 右值引用的使用场景和意义

右值引用只有在涉及拷贝的时候才有用。

  • 当传值返回时,返回的是右值,结和移动构造和移动赋值,可以减少拷贝。
String operator+(const String& s)
{
	String ret(s);
	//ret.append(s);//这里只是为了演示,没有实现append。
	return ret;//返回的是一个右值。
}
String& operator+= (const String & s)
{
	//this.append(s);//这里只是为了演示,没有实现append。
	return *this;//返回的是一个左值
}
int main()
{
    String s1("s1");
    String s2("s2");
    String s3 = s1+=s2;//假设s3来接收s1+=s2
    String s4 = s1+s2;
}

operator+()为传值返回,返回的是一个右值。operator+=()为传引用返回,返回的是一个左值。

String s3 = s1+=s2;//调用拷贝构造
String s4 = s1+s2;//调用移动构造

现实中,不可避免存在传值返回的场景,传值返回,返回的是返回对象的临时拷贝。

如果只实现了参数为const左值引用的拷贝构造,那么就会多次拷贝,降低效率。

但是如果实现了参数为右值引用的移动构造,就会减少拷贝的次数,提高效率

右值引用本身没有太多的意义,右值引用实现了移动构造和移动赋值,那么面对接收函数传值返回的对象(右值)等场景,可以提高效率。

  • 右值引用做函数参数

STL容器插入数据,结构基本也都是两个重载实现,一个左值引用,一个右值引用。

 

重载实现的插入函数参数为右值引用是如何提高效率的? 

5. 左值引用与右值引用总结

右值引用做参数和做返回值减少拷贝的本质是利用了移动构造和移动赋值

左值引用和右值引用本质的作用都是减少拷贝,右值引用本质可以认为是弥补左值引用不足的地方,他们两个相辅相成。

左值引用 ---->

解决的是传参过程中和返回值过程中的拷贝

  • 做参数:void push(T x) -> void push(const T& x) 解决的是传参过程中减少拷贝
  • 做返回值:T f2() -> T& f2()  解决的是返回值过程中的拷贝

左值引用不足:如果返回对象出了作用域就不在了,就不能用传引用返回,这个左值引用无法解决,所以需要C++11右值引用来解决。

右值引用---->

 解决的是传参后push/insert函数内部对象移动到容器上的问题和传值返回接受返回值时的拷贝。

  • 做参数:void push(T&& x)  解决的是push内部不再使用拷贝构造x到容器上,而是移动构造过去。
  • 做返回值:T f2()  解决的是外面调用接收f2() 返回对象时的拷贝,T ret = f2(),调用移动构造给ret,减少了拷贝。

6. 完美转发

模板中的T&& 不代表右值引用,而是万能引用,可以接收任何类型的参数,既能接收左值又能接收右值。但是仅仅使用T&& 并不能保证在函数内部或进一步传递参数时保持其原始的左值或右值属性。

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)
{
	//右值引用会在第二次之后的参数传递过程中丢失右值属性,再下一层调用会识别为左值
	Fun(t);
}
int main()
{
	PerfectForward(10);// 右值
	int a;
	PerfectForward(a);// 左值
	PerfectForward(std::move(a)); // 右值
	const int b = 8;
	PerfectForward(b);// const 左值
	PerfectForward(std::move(b)); // const 右值
	return 0;
}

要在传递过程中保持它的左值或右值属性,就需要用到完美转发

在函数模板中,将接收到的参数(无论是左值还是右值)以它们原本的类型(左值引用或右值引用)传递给另一个函数。

template<typename T>
void PerfectForward(T&& t)
{
	//std::forward<T>(t)在传参的过程中保持了t的原生类型属性。
	Fun(std::forward<T>(t));
}

 

上一篇:深入探讨移动Web开发:从基础到实践


下一篇:mark 一些攻防 prompt