目录
1、左值的定义
1.1 左值引用
2、右值的定义
2.1 右值引用
3、右值与左值的使用区别
4、右值引用的意义
4.1 左值引用的短板
5、移动语义
5.1 移动构造
5.2 移动赋值
6、万能引用
6.1 右值的别名-左值化
6.2 完美转发
前言:
在C++11之前就有了引用这个概念,只是全部都是作用于左值的,即给左值取别名。而到了C++11后,引入了右值引用的概念,即给右值取别名,无论是左值引用还是右值引用实际上都是给对象取别名,只不过定义左值与右值的概念不同。
1、左值的定义
第一眼看到左值可能会以为出现在‘=’左边的值都称为左值,的确出现在‘=’左边的值都可以称为左值,但是左值也可以出现在‘=’的右边,示例如下:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
int main()
{
int a = 1;
int c = a;//a是一个左值,但是他可以出现在‘=’右边
//c也是左值
return 0;
}
通常我们会认为可以被修改的值叫做左值,而在此之前我们会把一些不能被修改的值叫做常量(不认为其是左值),或者说其具有常属性(比如被const修饰了的左值),那么被const修饰过的左值确实不能被修改了,但是他依然是左值,原因在于凡是可以被赋值或被取地址的都叫做左值。示例代码如下:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
int main()
{
// 以下的p、a、c、*p都是左值
int* p = new int;//可以对p进行赋值或者取地址
int a = 1;//可以对a进行赋值或者取地址
const int c = 2;//虽然不能对c进行赋值,但是可以取其地址
return 0;
}
1.1 左值引用
左值引用顾名思义就是给左值进行取别名,示例如下:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
int main()
{
// 以下的p、a、c、*p都是左值
int* p = new int;//可以对p进行赋值或者取地址
int a = 1;//可以对a进行赋值或者取地址
const int c = 2;//虽然不能对c进行赋值,但是可以取其地址
// 以下几个是对上面左值的左值引用(侧面可以证明上述是左值)
int*& rp = p;//rp是一个指针的别名,他指向p指向的内容
int& ra = a;
const int& rc = c;
int& rvp = *p;//rvp是一块类型为int类型空间的别名
return 0;
}
2、右值的定义
左值可以出现在‘=’号的左右,但是右值只能出现在‘=’号的右边,因此意味着右值是不允许被直接修改,比如字面常量、函数的值返回、表达式的结果都是右值,并且不能够直接对他们进行修改,也不能够直接取右值的地址。
示例代码如下:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
double func(double x, double y)
{
return x > y ? y : x;
}
int main()
{
double x = 1.1, y = 2.2;
// 以下几个都是常见的右值
12;
x + y;
func(x, y);
// 以下三行代码会报错,原因就是不能够直接对右值进行修改
12 = 1;
x + y = 1;
func(x, y) = 1;
// 并且不能够直接取他们的地址
&12;
&(x + y);
&func(x, y);
return 0;
}
函数值返回和表达式结果为右值的原因如下图所示:
2.1 右值引用
左值引用的写法一般是在类型的右边加上一个取地址符号‘&’,而右值引用的写法是在类型的右边加上两个取地址符号‘&&’,并且对右值进行引用后,可以取到别名的地址,并且可以对别名进行修改。
示例代码如下:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
double func(double x, double y)
{
return x > y ? y : x;
}
int main()
{
double x = 1.1, y = 2.2;
// 以下几个都是常见的右值
12;
x + y;
func(x, y);
// 以下几个都是对右值的右值引用
int&& rr1 = 12;
double&& rr2 = x + y;
double&& rr3 = func(x, y);
//可以对别名进行修改和取地址
cout << ++rr1 << endl;
cout << &rr1 << endl;
cout << ++rr2 << endl;
cout << &rr2 << endl;
cout << ++rr3 << endl;
cout << &rr3 << endl;
return 0;
}
运行结果:
3、右值与左值的使用区别
1、左值引用符号是一个取地址符‘&’,右值引用符号是两个取地址符‘&&’。
2、左值可以对其进行赋值和取地址,右值不可以对其进行赋值和取地址。
3、左值可以出现在‘=’号的左右,但是右值只能出现在‘=’号的右边。
4、左值引用不能引用右值,右值引用不能引用左值。
5、在第4点的基础上,左值引用加上const后可以引用右值,右值引用可以引用被函数move调用后的左值。
上述第五点的测试代码如下:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
int main()
{
const int& a = 1;//左值引用右值
int b = 12;
int&& rb = move(b);//右值可以引用被move后的左值
rb = 1212;//并且可以通过rb更改b的值
cout << b << endl;
cout << rb << endl;
return 0;
}
运行结果:
4、右值引用的意义
既然左值引用被const修饰后可以引用右值,那么为什么还要引出右值引用这个概念呢?因为左值引用被const修饰后虽然可以引用右值,但是编译器区分不了被引用的对象是左值还是右值,最主要的是被修饰的右值不能够被修改了。体现这一细节的代码如下:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
void func(const int& a)//期望左值进该函数
{
cout << "void func(int& a)" << endl;
}
void func(int&& a)//期望右值进该函数
{
cout << "void func(int&& a)" << endl;
}
int main()
{
int a = 12;
func(a);
func(12);
return 0;
}
运行结果:
若没有右值引用,则上述的代码中,两次调用func函数都只能进入void func(const int& a),那么我们所期望的调用左值和右值所呈现不同内容的目的就实现不了了。
4.1 左值引用的短板
左值引用常常被用于函数的形参或函数的传引用返回,因为在这种场景下左值引用可以减少一些不必要的拷贝工作,可以节省空间和提供效率,但是传引用返回有局限性,即引用的对象的生命周期必须出了该函数栈帧还存在。因此当我们想返回一个局部对象给到外部时,就不能使用传引用返回了,因为涉及到权限越界访问的问题。
所以当返回局部对象时,会经过两重的拷贝构造,具体示意图如下:
从上图可以看到,临时对象的作用只是对st1对象进行深拷贝,并且拷贝任务完成后会销毁,如果能够把临时对象的内容充分利用到极致,就可以大大的提供效率了,而右值引用的作用在这里就体现出来了,对临时对象进行右值引用,则就可以对临时对象的内容进行修改了,通常会利用右值引用将临时对象里的数据和st1的数据进行交换,这样一来就省去了深拷贝这个步骤,并且把这一步叫做移动构造。
5、移动语义
移动语义即移动构造和移动赋值,他的作用主要是通过资源的交换从而避免一些繁琐的工作,达到提高效率的目的,实现移动语义的前提是右值引用。
5.1 移动构造
移动构造的思想是将右值中的资源进行转移,并用来初始化另一个对象,并且把未初始化对象的内容给到该右值,形象的来说就是窃取右值的资源来构造新的对象,这时候就无需进行深拷贝了。
移动构造其实是拷贝构造的一个函数重载,只不过拷贝构造的形参类型一般是const+左值引用,因为拷贝构造不希望修改被拷贝的对象。而移动构造的形参类型是没有加const的右值引用,可以通过其别名修改右值。具体示意图如下:
因此有了移动构造后,当右值要调用拷贝构造就会进入移动构造,而不是拷贝构造,比如上述例子:
5.2 移动赋值
移动赋值的思想和移动构造是一样的,也是通过资源的交换实现的。
移动赋值与拷贝赋值在写法上的区别:
移动赋值的实现代码:
// 移动赋值
string& operator=(string&& s)
{
swap(s);
return *this;
}
移动赋值的调用过程:
6、万能引用
万能引用又叫引用折叠,他可以引用左值和右值,但是他的写法和右值引用是一样的,只不过他作为一个模板参数,要作用在函数模板下。
万能引用写法如下:
template<typename T>
void Universal_citation(T&& t)
{
//此处的形参T&&表示的是万能引用,t可以接收左值和右值
//t接收右值时是右值引用,接收左值时被折叠成了左值引用
}
6.1 右值的别名-左值化
从上文可以得到,右值的别名是可以更改的,比如以上的移动构造和移动赋值,他们的形参都是一个右值引用,但是右值本身是不可以被更改的,当右值通过右值引用后,就可以通过别名来更改右值的内容,所以才能够在移动构造和移动赋值的函数中进行资源的交换。形象的来说,右值的别名可以看作是一个左值。
比如以下代码就可以证明右值别名的左值化:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
void func(int& x) { cout << "左值引用" << endl; }
void func(const int& x) { cout << "const 左值引用" << endl; }
void func(int&& x) { cout << "右值引用" << endl; }
void func(const int&& x) { cout << "const 右值引用" << endl; }
template<typename T>
void Universal_citation(T&& t)//此处的形参T&&表示的是万能引用,可以接收左值和右值
{
func(t);
}
int main()
{
Universal_citation(10);
int a;
Universal_citation(a);//左值
Universal_citation(move(a)); // 右值
const int b = 8;
Universal_citation(b);//const 左值
Universal_citation(move(b)); // const 右值
return 0;
}
运行结果:
从结果可以发现,调用函数Universal_citation的实参是右值,但是经过右值引用后,调用func函数的实参变成了左值,因此打印出来的都是”左值引用“。
6.2 完美转发
完美转发的写法如下:
std::forward<T>
他主要是用于参数在传递的过程中可以保留原来的属性,比如其可以让右值通过右值引用后,别名依然保留右值属性。注意:他的生效范围和move一样,只在当前行有效,比如:move(a),在当前行可以使a变成右值,但是到了下一行a还是左值。
将上述代码进行完美转发:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
void func(int& x) { cout << "左值引用" << endl; }
void func(const int& x) { cout << "const 左值引用" << endl; }
void func(int&& x) { cout << "右值引用" << endl; }
void func(const int&& x) { cout << "const 右值引用" << endl; }
template<typename T>
void Universal_citation(T&& t)//此处的形参T&&表示的是万能引用,可以接收左值和右值
{
func(std::forward<T>(t));
}
int main()
{
Universal_citation(10);
int a;
Universal_citation(a);//左值
Universal_citation(move(a)); // 右值
const int b = 8;
Universal_citation(b);//const 左值
Universal_citation(move(b)); // const 右值
return 0;
}
运行结果: