左值和右值
在以前,大家可能听说过“在赋值符号左边的就是左值,在赋值符号右边的就是右值”,这是错误的说法,例如将一个变量的赋值给另一个变量时,该表达式的左右两个值都是左值,左值引用其实就是给左值起一个别名
平常使用的引用传参(int&)不需要进行拷贝,按值传参需要进行拷贝(实参->形参),使用引用传参可以提高性能,在C++11引入了右值引用之后,这种引用方式就被称为左值引用
左值:表示可以获取地址的表达式,它能出现在赋值语句的左边,对该表达式进行赋值。
int a = 11;
const常量虽然可以取得地址,需要在声明时进行初始化,但是不可改变值
const int b = 11;
b = 12; //报错
右值:表示无法获取地址的表达式,有字面常量、函数返回值、表达式返回值、lambda表达式表达式返回值等(都是临时对象)。无法获取地址,所以不可以在表达式的左边,所以不能通过右值进行赋值,但不表示其不可改变,当定义了右值的右值引用时就可以更改右值。左值引用其实就是给右值起一个别名,使得右值可以进行赋值改变
int fun()
{
int a = 1;
return a;
}
fun() = 2; //报错
int 'hello' = 'hi'; //报错
总结:所以区分左值与右值的规则是:“左值可以取地址,右值不能取地址”,因为左值一般是被放在内存地址空间中,有明确的存储地址;而右值一般可能是计算中产生的中间值、临时变量,也可能是被保存在寄存器上的一些值,总的来讲就是,右值并没有被保存在内存地址空间中,也就无法取地址。
左值引用和右值引用
左值引用:传统的C++中引用被称为左值引用,作用是起别名(绑定)。
在类型的右边加一个&即可
int a = 10;
int& b = b;
右值引用:C++11中增加了右值引用,右值引用关联到右值时,右值引用指向右值存储的地址。右值引用可以获取该地址表示临时对象的存储位置。
在类型的右边加俩两个&
int&& a = 10;
左值引用的引用范围
普通的左值引用能引用左值,但是加上const后就变成万能引用,既可以引用左值又可以引用右值
const int& a = 10
因为右值都是不能被修改的值,左值引用加上const后也不能修改了,否则权限放大,因此会报错。但左值引用加上const后权限一致,此时是权限平移,就可以引用右值了
右值引用的引用范围
右值引用只能引用右值,但是左值可以使用move()函数,将左值转换为右值,就可以对其进行引用
int a = 10;
int& b = a;
int&& c = move(b);
右值引用的作用
解决传值返回拷贝时的局部变量出作用域生命周期结束的问题
传值返回需要进行两次拷贝,有的情况下,在编译器优化后只需要进行一次拷贝,省略了临时拷贝,而传引用返回则不需要进行拷贝
当使用以下左值引用时,const将左值引用变为万能引用,即可以接收左值也可以接收右值,并且节省了传参以及返回值的拷贝
const int& func(const int& a)
{
return a;
}
但是当返回值是函数内的局部变量时,因为函数返回后出了作用域,生命周期就结束了,局部变量的生命周期也就结束了,此时如果在调用函数时接收了函数返回值,那么将出现非法寻址。
此时不能使用传引用返回,只能使用传值返回,如果返回的是一个包含大量数据的容器,那么拷贝的消耗很高。
const int& func(const int& a)
{
int b = a*2;
return b;
}
解决方式一:使用堆内存,用传指针的方式传递数据,则需要考虑将堆内存回收的时机,否则将造成内存泄露
解决方式二:使用输出型参数来解决,遇到参数多且重载函数也多的情况,并不好使用
解决方式三:使用右值引用的方式
const int&& func(const int& a)
{
int b = a*2;
return b;
}
右值的分类
普通的右值称为纯右值
即将被销毁的右值称为将亡值
在右值引用中,如果某些右值是将亡值,可以将亡值与接收值及进行交换,因为是直接交换两个对象中的数据,所以不会进行拷贝。通过这种方式,就可以在数据量非常庞大的情况下提高效率。但是使用不当有可能造成变量的值被修改
int main()
{
string s1("hello");
string s2(move(s1));
system("pause");
return 0;
}
使用字符串常量"hello"构造了s1后,在构造s2时,s1被转为右值,用于初始化s2,因为输入参数是右值,所以调用了s2的移动拷贝构造,交换了s1和s2的值,导致s1的值被置空(因为原来s2就是空的)
所以使用右值引用移动构造的方法,只适用于将亡值,因为将亡值马上要被销毁释放,被交换了任何值都无影响
移动赋值同理,也是采用交换数据的方式进行赋值
有了右值后,返回值是左值时会被编译器识别为右值,然后进行移动拷贝,对拷贝过程进行了优化
stl容器的右值引用
很多容器的插入元素的接口函数都提供了右值引用的重载函数:
list::push_back
void push_back(value_type&& val);
vector::push_back
void push_back(value_type&& val);
map::insert
template<class P> pair<iterator,bool> insert(P&& val);
右值可以被右值引用间接修改
右值不能直接修改,但是右值被右值引用进行引用后,就可以被间接修改
int&& num = 10;
++num;
右值不能被修改是因为右值不能被取地址(或者说是没有地址),但是在右值被右值引用进行引用后,就被存储到栈上,有了地址且可以取得地址,因此就可以被修改了。也可以加上const关键字使得右值引用不可修改
在上面讲了可以通过右值引用,将将亡值的数据与接收对象的数据进行交换。但是将亡值是右值,不能取得地址,无法和接收对象的数据完成交换,所以需要使用右值引用,才可以取得地址,完成交换
要注意,虽然右值引用后右值可以被修改,但它修改的并不是对应的常量,而是在对应地址上存储的值。因为常量是不允许修改的
右值引用后的值被视为左值
int func(int&& num)
{
list.push_back(num);
}
调用func函数,当实参为右值时调用形参为右值引用的func函数,此时形参num将被视为左值,调用list.push_back时,将不会调用移动拷贝,而调用深拷贝,影响效率
解决:
使用move(),将左值转为右值即可
void func(int&& num)
{
list.push_back(move(num));
}
完美引用
如果函数需要重载参数是右值引用或左值引用两个版本的函数,可通过使用"完美引用"一个函数解决,但仅限于有模板的情况
template<class T>
void func(T&& num)
{
list.push_back(move(num));
}
当使用模板时,既可以接收右值,又可以接收左值,其实在传入左值的时候会折叠一个&,相当于T&
完美转发
上文使用move保持num的右值特性,如果函数内要多次使用num,将会比较麻烦,而万能引用只是在传入时识别两种变量,但是在右值二次引用时依旧被视为左值。如果要传入的右值被二次使用时还需要保持右值特性,则需要使用"完美转发"
void func(int&& x)
{
cout<<"右值引用"<<endl;
}
void func(int& x)
{
cout<<"左值引用"<<endl;
}
template <class T>
void PerfectForward(T&& x)
{
func(std::forward<T>(x));
}
使用std::forward<T>在传参的过程中保留对象原生类型的特性,也就是传入左值,第二次传递也是左值;传入右值,第二次传递也是右值