一.左值引用和右值引用
什么是左值引用,什么是右值引用
左值是一个表示数据的表达式(变量名解引用的指针),我们可以获取到它的地址,可以对它赋值,左值可以出现在符号的左边。使用const修饰后,不能给他赋值,但是可以取地址。左值引用就是对左值的引用,就是给左值取别名
右值也是一个表示数据的表达式(如字面常量,表达式返回值(但不能是左值引用返回!!!!!)),右值引用可以出现在符号右面,但不能出现在符号左边,不能取地址。右值引用就是给右值取别名
int add(int x, int y) {
return x + y;
}
//左值引用和右值引用
int main()
{
//左值
int a = 10;
int* p = new int(20);
//左值引用
int aa = a;
int*& pp = p;
int x = 1;
int y = 2;
//右值
10;
x + y;
add(x, y);
//右值引用
int&& rr1 = 10;
int&& rr2 = x + y;
int&& rr3 = add(x, y);
}
但是注意,右值被引用后,会导致右值被存储到特定的位置,而且可以取到该位置的地址,并且可以修改右值引用rr1,但是要是不想被修改,可以用const的右值引用去接收:
左值引用右值引用比较
如上,左值引用不能直接引用右值,但是const左值引用可以引用右值
如上,右值引用不能饮用左值,但是可以引用move之后的左值
因为move的底层就是用到了类型转换中的static_cast。move调用后,右值就能变成左值
二.右值引用使用场景——移动构造移动赋值
1.左值引用的短板
左值引用在传参方面做的很好,但是在函数返回方面:如果返回的对象是一个局部变量,就不能使用左值引用返回了,就得先拷贝构造临时变量,再拷贝构造出返回值(不过编译器进行了优化,优化成一次构造)。
比如下面这个:
这时右值引用就派上用场了,如果写了移动构造,返回的时候就会直接进行数据交换。
先说一下右值到底有啥,首先就是纯右值,其次就是将亡值,比如函数中的局部变量在函数结束时会被释放,所以局部变量这时候就会被编译器看作右值
也就是说,上面的ret是一个将亡值,当我们写了移动构造后,他就是走移动构造这个函数。
2.移动构造
什么是移动构造,移动构造的本质就是将参数右值的资源窃取过来,占为己有。此时就不用去做深拷贝了,如下就是移动构造
如上,直接交换对象中的每一个成员变量即可(swap是咱们自己实现的)
移动赋值也一样:
这次的构造就是使用的移动构造,它能够把ret指向的堆上的资源给留下来
原来是这样的:
现在是这样的:
也就是说,传返回值时,也是会有临时对象的拷贝,但由于识别成将亡值,所以不管是临时对象还是最终的结果,都是使用的移动构造,对于其中的开始结束指针,都是指向同一块空间
通过监视也能看出来:
首先,这是进入tostring函数后ret的监视
这是接收到的string对象,会发现两者指向同一块空间
总结
1.当既有移动构造又有拷贝构造既有移动赋值又有赋值重载时,如果你是右值,那就直接转移资源,如果你是左值那就进行深拷贝
2.右值引用到底在什么时候能够体现其价值?当此自定义类型中需要进行深拷贝时就有其价值。
3.c++98和c++11用法效率比较:
int main()
{
//c++98
vector<int> ret;
func(ret);
//c++11
vector<int> ret=func();
}
如上,都想给vector中存放结果,对于c++98来说,只有如上这样做才能提高效率,而c++直接接收返回值即可,因为stl容器基本上都实现了移动构造移动赋值
4.stl中的插入等都加入了右值版本
3.插入操作的右值引用版本
我们就以list为例
首先给节点的构造函数加一个右值版本,不过库里面参数是直接写成了 T&& data,没有加const和缺省值,然后先修改push_back和insert,都在原始的基础上添加一个右值版本
然后尾插调试测试一下:
发现进入push_back时进入的是右值版本,但是进入insert时进入到是左值版本!!因为前面说过,右值被引用后,右值引用就是左值了!!!。那怎么办??可以加上move
那么凡是涉及到右值传递的,传递完后都应该加上move,如下:
还有一个地方需要注意:
这里data的构建!!data可能是内置类型,也可能是自定义类型,也可能是容器,所以它也应该加上move,只要它对应的类中写了移动构造。就能提高效率
4.右值引用使用注意
当用右值引用去构造对象后,右值引用就不存在了,如下例子:
s1没打印出来。为啥呢?因为移动构造其实就是资源转移,它把s1的资源转移给了s3,所以s1就被置空了
三.完美转发
1.万能引用
在普通函数中&&只能是接收右值引用。但是在模板中,&&表示万能引用,也就是说,当参数是左值时,它就是左值引用,当参数是右值时,他就是右值引用。
2.完美转发
void func(int& t)
{
cout << "左值引用" << endl;
}
void func(const int& t)
{
cout << "const左值引用" << endl;
}
void func(int&& t)
{
cout << "右值引用" << endl;
}
void func(const int&& t)
{
cout << "const右值引用" << endl;
}
template<class T>
void perfectForward(T&& t)
{
func(t);
}
int main()
{
int a = 10;
const int b = 20;
perfectForward(a);
perfectForward(b);
perfectForward(move(a));
perfectForward(move(b));
}
按照上面万能转发的规则,程序的结果应该是前俩个是左值后俩个是右值,但结果如下:
这是因为右值被引用后就变回了左值。那么如何保持右值的属性呢?就可以用到完美转发:
完美转发能够保证变量在传递过程中的属性,这次结果就是想要的了:
上面对push_back的实现就可以不再使用move而去使用forward<T>