右值VS左值
关于什么是右值什么是左值,我们是这样判断的:
- 右值:不能出现在等号左边的值 且 不能取地址(包括:字面常量、内置类型表达式,内置类型返回值)
- 左值:可以取地址(包括:变量)
下面几种表述是错误的:
-
赋值符号左边的就是左值——与下面的同一个例子😅
-
能修改的就是左值——自定义类型且重载了operator+
std::string s1("123"); std::string s2("456"); std::string s3("789"); s1 + s2 = s3;
这是因为重载了
+
号,传值返回会生成一个匿名对象,然后s3是对这个匿名对象赋值了,但是这个匿名对象在这行结束之后就会销毁 -
在赋值符号右边的就是右值——左值既能出现在左边又能出现在右边
int add(int x, int y)
{
return x + y;
}
int a1 = 1; //1
int* a2 = new int[10]; //2
const int a3 = 1; //3
int a4 = a1 + a3; //4
int a5 = add(a1, a3); //5
1、2、4、5等号右边的都是右值
1、2、3、4、5等号左边都是左值
右值又可以细分为:
- 纯右值:内置类型表达式
- 将亡值:自定义类型的表达值,即将被析构
这两个是后面识别右值的判别准则
右值引用VS左值引用
定义
- 右值引用:对右值的引用
- 左值引用:对左值的引用
注意区分:右值 VS 右值引用 和 左值 VS 左值引用
//左值 和 右值
int a1 = 1;
int* a2 = new int[10];
const int a3 = 1;
int a4 = a1 + a3;
int a5 = add(a1, a3);
//右值引用
int&& r1 = 10;
int&& r2 = a1 + a3;
int&& r3 = add(a1, a3);
//左值引用
int& l1 = a1;
const int& l2 = a3;
int*& l3 = a2;
注意:
- 右值是不可以取地址,但是右值引用是可以取地址,因为给右值取别名之后必然会存储到特定位置,也就是说右值引用是左值!📌
- 左值引用既可以引用左值也可以引用右值——引用右值可以使用
const int &a=1;
- 右值引用只能引用右值,不能引用左值
- 右值引用可以引用move之后的左值
move函数
move函数的唯一作用就是将左值强制转换成右值,但是使用move时将自定义类型转成右值的时候一定要注意:如果该类型支持移动赋值 或 移动构造,可能会产生一些意想不到的后果
string s1("i am student");
string s2(move(s1));
cout << s1 << s1.size() << endl;
cout << s2 << s2.size()<<endl;
这里s1的空间就被释放掉了,所以move自定义类型的时候一定要谨慎!
左值引用&&右值引用 与 函数重载
左值引用和右值引用可以作为函数重载的依据,
- 如果参数是左值—— 匹配左值引用
- 如果参数是右值——优先匹配右值引用
- 例如
const int&
这种既能匹配左值又能匹配右值的,右值会优先匹配右值引用,如果没有右值引用才会匹配它。
void fun(int& a)
{
cout << "int& a" << endl;
}
void fun(const int& a)
{
cout << "const int& a" << endl;
}
void fun(int&& a)
{
cout << "const int&& a" << endl;
}
int main()
{
int a = 1;
const int b = 2;
fun(a);
fun(b);
fun(1);
}
模板
template<class T>
void forward(T&& t)
{
//.........
}
在函数模板的情况下,传入不同类型的参数会如何?
模板里面的函数参数不论是&
还是&&
既可以实例化成左值引用,又可以实例化成右值引用。
例如如下几个例子:
int a = 1;
const int b = 1;
forward(a); //这里T 为int 实例化后的函数参数为:int & t
forward(move(a)); //这里T 为int 实例化后的函数参数为:int&& t
forward(b); //这里T 为const int 实例化后的函数参数为:int & t
forward(move(b)); //这里T 为const int 实例化后的函数参数为:int&& t
完美转发
我们用模板实例化的函数不管传入的是左值还是右值,在引用之后通通变成了左值。如何让函数模板在向其他函数传递参数时该如何保留该参数的左右值属性?
看一下下面这种情况:
void fun(int& a)
{
cout << "int& a" << endl;
}
void fun(const int& a)
{
cout << "const int& a" << endl;
}
void fun(int&& a)
{
cout << "int&& a" << endl;
}
void fun(const int&& a)
{
cout << "const int&& a" << endl;
}
template<class T>
void forward(T&& t)
{
fun(t);
}
int main()
{
int a = 1;
const int b = 1;
forward(a); //这里T 为int 实例化后的函数参数为:int & t
forward(move(a)); //这里T 为int 实例化后的函数参数为:int&& t
forward(b); //这里T 为const int 实例化后的函数参数为:int & t
forward(move(b)); //这里T 为const int 实例化后的函数参数为:int&& t
}
结果我们发现结果为:
这是由于在引用之后丢失了右值的特性,我们可以使用完美转发:
void fun(int& a)
{
cout << "int& a" << endl;
}
void fun(const int& a)
{
cout << "const int& a" << endl;
}
void fun(int&& a)
{
cout << "int&& a" << endl;
}
void fun(const int&& a)
{
cout << "const int&& a" << endl;
}
template<class T>
void forward(T&& t)
{
fun(forward<T>(t));
}
int main()
{
int a = 1;
const int b = 1;
forward(a); //这里T 为int 实例化后的函数参数为:int & t
forward(move(a)); //这里T 为int 实例化后的函数参数为:int&& t
forward(b); //这里T 为const int 实例化后的函数参数为:int & t
forward(move(b)); //这里T 为const int 实例化后的函数参数为:int&& t
}
这时传入参数的特性就可以获得很好的保留:
左值引用的意义
string function(string& s)
{
string tmp;
//.........
return tmp;
}
左值引用广泛引用于函数传参,可以不用拷贝,提升了效率,但是你在函数内对s修改同时会影响到外部s。
但是返回的时候一般的情况下是需要拷贝的,如果函数返回值是引用,那么就说明返回的这个对象的生命周期要大于函数的栈帧,也就是说返回的对象出了函数作用域不能被销毁,像上文中的tmp是无法使用引用传值返回的,tmp在函数还未返回时就会调用析构函数销毁
现在有如下一个情况:如果返回的tmp经过函数内部的操作变成一个无比巨大的字符串,如果返回必然会调用拷贝构造函数,造成很大的开销。有没有什么好方法减少开销?
-
在没学右值引用时,我们使用输出型参数,在函数作用域外将返回的对象开好,再在函数参数列表引用传入,这时就可以跳过传值返回不必要的那次拷贝构造了。
void function(string& s,string& tmp) { //......... }
右值引用又为我们提供了一个新的方法
移动构造&&移动赋值
我们来重新看一下这个函数的返回过程:
string s1("123");
string ret=function(s1); //4
string ret;
ret=function(s1); //5
-
调用拷贝构造函数将tmp深拷贝给另一个临时对象,
- 如果输出形式为4,那么编译器会将赋值重载优化掉,也就是将tmp直接拷贝构造给外部的ret
- 如果输出形式为5,还会调用一次赋值重载(实际上也是深拷贝)实现ret赋值
-
走到2的位置处,函数结束,销毁函数栈帧调用string的析构函数销毁tmp
实际上tmp里面存的就是ret需要的内容,但是ret出函数栈帧需要被销毁,所以需要将其深拷贝出来,再赋值给ret。
能不能让tmp中直接赋值给ret呢?——移动构造&&移动赋值
string(string&& s) //右值引用 移动构造
{
std::swap(_str, s._str);
std::swap(_size,s._size);
std::swap(_capacity,s._capacity);
}
string& operator=(string && s) //右值引用 移动赋值
{
swap(*this, s);
return *this;
}
如果我们在string中添加如上两个函数,4、5两次赋值或拷贝会发生什么变化?
函数在返回tmp时,由于tmp出函数作用域就会调用析构函数销毁,所以会被识别成将亡值。将亡值属于右值,匹配 移动构造 和 移动赋值。
实际上过程还是和上面一模一样但是所有的拷贝构造都变成了移动构造,所有的赋值重载变成了移动赋值。
但是我们发现移动构造 和 移动拷贝 只是交换了资源,并没有开辟新的资源,将即将销毁的tmp中的内容与别的对象交换,从而实现资源的转移。比传统的深拷贝要节约了不少资源。
默认移动构造&&赋值
上面介绍了两个新的函数——移动构造函数 和 移动赋值函数,这两个也属于类的默认成员函数。
-
默认移动构造
生成条件:- 没有自己实现移动构造
- 没有实现析构函数、拷贝构造、拷贝赋值重载
那么编译器会默认生成一个 ,对于内置类型会按字节拷贝,对于自定义类型会调用该类型的移动构造(如果存在的话,不存在的话就是拷贝构造)
-
默认移动赋值
生成条件:- 没有自己实现移动赋值
- 没有实现析构函数、拷贝构造、拷贝赋值重载
那么编译器会默认生成一个 ,对于内置类型会按字节拷贝,对于自定义类型会调用该类型的移动赋值重载(如果存在的话,不存在的话就是普通赋值重载)