一、左值和右值
所有的c++表达,不是左值就是右值。
左值是指存在于单个表达式之外的对象(如变量),而右值则是暂时存在于单个表达式之内的对象(如字面常量、函数或表达式的返回值)。通俗来说就是,左值的生存期不只是当前表达式,后面还能用到它;而右值出了当前表达式就挂了,又称为将亡值。
左值在内存中占有确定的地址,可以获取它的地址,可以对它进行赋值,左值可以在赋值符号的左边或者右边。右值不在内存中占有确定地址(不是不占地址,只是我们找不到),不可以获取它的地址,通常是不可改变的值,它只能在赋值符号的右边。
二、左值引用和右值引用
(1)概念
左值引用就是给左值取别名,右值引用就是给右值取别名。
左值引用:只能引用左值,就是我们通常使用的引用
const 左值引用:可以引用左值,也可以引用右值(因为右值通常是不可改变的值)
右值引用:只能引用右值,左值可以通过move(左值)转换为右值,继而使用右值引用
右值引用是左值,可以取地址、可以修改值
const右值引用:如果想限制右值引用不被修改,可以加const修饰
int main() {
// 左值引用只能引用左值,不能引用右值。
int a = 10;
int& ra1 = a; // ra1为a的别名
//int& ra2 = 10; // 编译失败,因为10是右值
// const左值引用既可引用左值,也可引用右值。
const int& ra3 = 10;
const int& ra4 = a;
//右值引用只能右值,不能引用左值。
int&& r1 = 10;
int a = 10;
//message : 无法将左值绑定到右值引用
int&& r2 = a;
//右值引用可以引用move以后的左值
int&& r3 = std::move(a);
++r1;
const double&& rr2 = 10;
//rr2++; //不可以修改
return 0;
}
(2)作用和价值
左值引用的意义在于:
1、函数传参:引用传递可以避免实参传递给形参时发生拷贝
2、函数返回值:对于出了作用域还存在的对象,同样可以通过返回引用类型避免拷贝
但函数传返回值时,对于出了作用域就销毁的对象,左值引用不能解决问题。此时有两种方式可以避免拷贝:使用输出型参数和右值引用。
对于出了作用域就销毁的对象,函数传返回值时,会发生两次拷贝构造(return时先将返回值拷贝给一个临时变量,再由临时变量拷贝给函数返回值的赋值对象)。有的编译器会自动优化,将连续的两个拷贝构造优化为一个,直接跳过中间的临时变量。
右值引用可以补齐函数传返回值时发生拷贝构造的短板:
传左值时,会调用拷贝构造函数。
传右值时,会调用移动构造函数,移动构造函数会直接将this和右值(将亡值)交换资源。
有了移动构造之后,函数返回右值(move(左值)),可以实现0拷贝。
上述是针对拷贝初始化,对于拷贝赋值,是用一个已存在的对象接受返回值,此时编译器无法进行优化,需要中间生成一个临时变量。此时函数返回右值,会先调用移动构造生成一个临时变量,然后再调用移动赋值将临时变量赋值给已存在的对象。
区分:直接初始化(一般使用 ( ) 来定义对象)、拷贝初始化(一般使用 = 来定义对象)和拷贝赋值(对已经初始化完成的对象进行修改)
一句话理解精髓就是,由于左值还要继续使用,所以我们只能对其进行拷贝构造;而右值是将亡值,我们可以直接对其进行资源交换,从而避免拷贝。
三、万能引用和完美转发
对于参数类型是右值引用的函数,我们在传右值时,右值引用会改变右值的特性,将其变为左值。如果我们想要继续调用右值引用的函数,则需要不断的move将左值转化为右值。
万能引用:当不明确规定传左值还是右值时,万能引用起到了用处,可以随便传入左值或右值。参数中的&&不是右值引用,而是为了万能引用,可以折叠。当传左值时,就把&&折叠为&。
// 万能引用
template<typename T>
void PerfectForward(T&& t) {
// t 可能是左值也可能是右值
// func(move(t));
// 完美转发,保持属性
func(std::forward<T>(t));
}
走到调用Func()时,还是会因为由于右值引用导致右值转化为左值,所以又引出了完美转发,完美转发可以自动识别我们传的是左值还是右值,并保持属性,不会因为右值引用而改变右值属性。
参考:https://blog.csdn.net/ChaoFreeandeasy_/article/details/130229252